@typesol/cli 0.23.0 → 0.24.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.
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
- import { MAX_RETURN_DATA, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
3
- import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
2
+ import { MAX_RETURN_DATA, calleeName, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
3
+ import { INT_INFO, isScalar, isWritable, optionKey, scalarSize, typeName, } from "../model.js";
4
4
  import { U64, BOOL, cString, fixedVecC, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
5
5
  import { CpiEmitter } from "./cpi.js";
6
6
  /**
@@ -276,6 +276,15 @@ export class StatementEmitter extends CpiEmitter {
276
276
  this.loops.pop();
277
277
  });
278
278
  }
279
+ /** The function an identifier names (a closure, a local function or a helper), as something to expand; else undefined. */
280
+ functionNamed(id) {
281
+ const l = this.lookup(id.text);
282
+ if (l)
283
+ return l.closure;
284
+ const name = calleeName(id, this.model.helperDecls, this.checker);
285
+ const h = name ? (this.model.helpers.get(name) ?? this.model.generics.get(name)) : undefined;
286
+ return h ? { node: h.node, name: h.display, scopes: [], env: this.noEnv(), imports: h.imports } : undefined;
287
+ }
279
288
  /** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
280
289
  isPipeline(e) {
281
290
  return ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && (e.expression.name.text === "map" || e.expression.name.text === "filter");
@@ -343,7 +352,13 @@ export class StatementEmitter extends CpiEmitter {
343
352
  let idx = indexId;
344
353
  let k = 0;
345
354
  for (const st of stages) {
346
- const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes] };
355
+ if (st.kind === "map") {
356
+ const ret = st.fn.body && !ts.isBlock(st.fn.body) ? st.fn.body : undefined;
357
+ const bare = ret && ts.isParenthesizedExpression(ret) ? ret.expression : ret;
358
+ if (bare && ts.isObjectLiteralExpression(bare))
359
+ this.fail(st.fn, "a map to structs is stored, with the struct type: const ys: FixedVec<MyStruct, N> = xs.map((x) => ({ … }))");
360
+ }
361
+ const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
347
362
  const v = this.expandClosure(closure, [element, idx].slice(0, st.fn.parameters.length), st.fn);
348
363
  if (st.kind === "filter") {
349
364
  if (!v)
@@ -393,8 +408,13 @@ export class StatementEmitter extends CpiEmitter {
393
408
  const n = args[1] && ts.isLiteralTypeNode(args[1]) && ts.isNumericLiteral(args[1].literal) ? Number(args[1].literal.text) : NaN;
394
409
  const elemNode = args[0];
395
410
  const sn = elemNode && structNameOf(elemNode, this.scope);
411
+ const fs = elemNode && typesolName(elemNode, this.scope.imports) === "FixedString" ? elemNode.typeArguments?.[0] : undefined;
412
+ const fcap = fs && ts.isLiteralTypeNode(fs) && ts.isNumericLiteral(fs.literal) ? Number(fs.literal.text) : NaN;
413
+ if (Number.isInteger(fcap) && fcap >= 1)
414
+ this.model.fstrings.add(fcap);
396
415
  annotatedElem = elemNode
397
- ? (resolveScalarType(elemNode, this.scope) ?? (sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : undefined))
416
+ ? (resolveScalarType(elemNode, this.scope) ??
417
+ (sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : Number.isInteger(fcap) && fcap >= 1 ? { kind: "fstring", cap: fcap } : undefined))
398
418
  : undefined;
399
419
  if (!ts.isTypeReferenceNode(t) || typesolName(t, this.scope.imports) !== "FixedVec" || !annotatedElem || !Number.isInteger(n) || n < 1)
400
420
  this.fail(t, "annotate the new list as FixedVec<T, N> (N a literal)");
@@ -415,21 +435,39 @@ export class StatementEmitter extends CpiEmitter {
415
435
  this.line(`/* ${c} */`);
416
436
  let elem = stages.some((st) => st.kind === "map") ? undefined : this.listElem(base);
417
437
  const cap0 = cap;
418
- this.eachElement(init, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element) => {
419
- if (!elem) {
420
- const v = this.expr(element);
421
- elem = v.type;
422
- }
423
- const target = annotatedElem ?? elem;
424
- const sub = { lv: `${c}.items[${c}.len]`, type: target, writable: true };
425
- this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
426
- const vt = this.storageValueType(target);
427
- if (vt)
428
- this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(element, vt), vt, element)).c) + ";");
429
- else
430
- this.writeAggregate(sub, element);
431
- this.line(`${c}.len++;`);
432
- });
438
+ // A last map to structs or FixedStrings (annotated) writes each result straight into its slot.
439
+ const last = stages.at(-1);
440
+ if (last?.kind === "map" && annotatedElem && !isScalar(annotatedElem)) {
441
+ const target = annotatedElem;
442
+ const inner = init.expression.expression;
443
+ this.eachElement(inner, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element, index) => {
444
+ this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
445
+ const closure = { node: last.fn, name: "map's callback", scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
446
+ this.expandClosure(closure, [element, index].slice(0, last.fn.parameters.length), last.fn, {
447
+ lv: `${c}.items[${c}.len]`,
448
+ type: target,
449
+ writable: true,
450
+ });
451
+ this.line(`${c}.len++;`);
452
+ });
453
+ elem = target;
454
+ }
455
+ else
456
+ this.eachElement(init, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element) => {
457
+ if (!elem) {
458
+ const v = this.expr(element);
459
+ elem = v.type;
460
+ }
461
+ const target = annotatedElem ?? elem;
462
+ const sub = { lv: `${c}.items[${c}.len]`, type: target, writable: true };
463
+ this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
464
+ const vt = this.storageValueType(target);
465
+ if (vt)
466
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(element, vt), vt, element)).c) + ";");
467
+ else
468
+ this.writeAggregate(sub, element);
469
+ this.line(`${c}.len++;`);
470
+ });
433
471
  const t = { kind: "vec", elem: annotatedElem ?? elem, cap: cap0 };
434
472
  const bytes = 4 + this.fieldBytes(t.elem) * cap0;
435
473
  if (bytes > 2048)
@@ -484,13 +522,17 @@ export class StatementEmitter extends CpiEmitter {
484
522
  * was declared with, so a block around the call that declares the same name does not change what it reads, and
485
523
  * writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
486
524
  */
487
- expandClosure(c, args, node) {
525
+ expandClosure(c, args, node, into) {
488
526
  const fn = c.node;
489
527
  if (this.inlining.has(fn))
490
528
  this.fail(node, `${c.name} calls itself: closures cannot be recursive`);
529
+ // Its types are written in its own file: they mean what that file's imports say (the arguments are the caller's).
530
+ const calleeScope = c.imports === this.scope.imports ? this.scope : { ...this.scope, imports: c.imports };
491
531
  if (args.length !== fn.parameters.length)
492
532
  this.fail(node, `${c.name}() takes ${fn.parameters.length} argument(s)`);
493
533
  const params = new Map();
534
+ // Account parameters (of a helper expanded here) name the caller's accounts while the body runs.
535
+ const aliases = new Map();
494
536
  fn.parameters.forEach((p, i) => {
495
537
  if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
496
538
  this.fail(p, "closure parameters are plain names");
@@ -501,12 +543,22 @@ export class StatementEmitter extends CpiEmitter {
501
543
  params.set(name, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
502
544
  return;
503
545
  }
546
+ if (p.type && ts.isIdentifier(arg) && !this.lookup(arg.text) && this.accounts.has(arg.text) && !resolveValueType(p.type, calleeScope)) {
547
+ aliases.set(name, this.accounts.get(arg.text));
548
+ return;
549
+ }
504
550
  const c2 = `l_${name}_${this.temp++}`;
505
- const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
551
+ const annotated = p.type ? resolveValueType(p.type, calleeScope) : undefined;
506
552
  if (!annotated && this.looksLikeStructPlace(arg)) {
507
553
  const place = this.structPlace(arg);
508
554
  this.line(`TSS_${place.struct} *${c2} = ${place.ptr};`);
509
- params.set(name, { c: c2, type: { kind: "structRef", struct: place.struct, writable: place.writable }, mutable: false, paths: this.pathsOf(arg) });
555
+ params.set(name, {
556
+ c: c2,
557
+ type: { kind: "structRef", struct: place.struct, writable: place.writable },
558
+ mutable: false,
559
+ paths: this.pathsOf(arg),
560
+ decl: p,
561
+ });
510
562
  return;
511
563
  }
512
564
  let v = this.expr(arg, annotated);
@@ -522,7 +574,7 @@ export class StatementEmitter extends CpiEmitter {
522
574
  this.line(`${valueC(type)}${type.kind === "account" ? "" : " "}${c2} = ${this.ownedString(v).c};`);
523
575
  params.set(name, { c: c2, type, mutable: true });
524
576
  });
525
- const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, this.scope) : undefined;
577
+ const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, calleeScope) : undefined;
526
578
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword && !declared)
527
579
  this.fail(fn.type, "closures return integers, booleans, Pubkeys, strings, enums or Options");
528
580
  const frame = {
@@ -532,10 +584,34 @@ export class StatementEmitter extends CpiEmitter {
532
584
  declAt: this.lines.length,
533
585
  pad: " ".repeat(this.indent),
534
586
  node: fn,
587
+ into,
535
588
  };
536
589
  this.line(declared ? `${declared.kind === "pubkey" ? "SolPubkey" : valueC(declared)} ${frame.result};` : `/* ${frame.result} */`);
537
- const saved = { scopes: this.scopes, loops: this.loops.splice(0) };
590
+ // The body names what it could where it was written: a closure the caller's arguments and accounts, a helper
591
+ // only its own parameters (its account parameters name the caller's accounts) and the module.
592
+ const swap = (m, to) => {
593
+ const before = new Map(m);
594
+ m.clear();
595
+ for (const [k, v] of to)
596
+ m.set(k, v);
597
+ return before;
598
+ };
599
+ const bodyAccounts = new Map(c.env.accounts);
600
+ for (const [n, a] of aliases)
601
+ bodyAccounts.set(n, a);
602
+ // Accounts are found by name before locals: a parameter named like one the body can see would read the account.
603
+ for (const n of params.keys())
604
+ if (bodyAccounts.has(n))
605
+ this.fail(fn.parameters.find((p) => ts.isIdentifier(p.name) && p.name.text === n) ?? node, `"${n}" shadows the account "${n}"; use another name`);
606
+ const saved = {
607
+ scope: this.scope,
608
+ scopes: this.scopes,
609
+ loops: this.loops.splice(0),
610
+ args: swap(this.args, c.env.args),
611
+ accounts: swap(this.accounts, bodyAccounts),
612
+ };
538
613
  this.scopes = [...c.scopes, params];
614
+ this.scope = calleeScope;
539
615
  this.inlining.add(fn);
540
616
  this.inlineFrames.push(frame);
541
617
  try {
@@ -551,7 +627,7 @@ export class StatementEmitter extends CpiEmitter {
551
627
  const ret = ts.factory.createReturnStatement(body);
552
628
  ts.setTextRange(ret, body);
553
629
  ret.parent = fn;
554
- if (ts.isCallExpression(body) && !frame.type) {
630
+ if (ts.isCallExpression(body) && !frame.type && !into) {
555
631
  const v = this.call(body, false);
556
632
  if (v)
557
633
  this.closureResult(v, body, frame);
@@ -566,7 +642,10 @@ export class StatementEmitter extends CpiEmitter {
566
642
  finally {
567
643
  this.inlineFrames.pop();
568
644
  this.inlining.delete(fn);
645
+ swap(this.args, saved.args);
646
+ swap(this.accounts, saved.accounts);
569
647
  this.scopes = saved.scopes;
648
+ this.scope = saved.scope;
570
649
  this.loops.push(...saved.loops);
571
650
  }
572
651
  if (!frame.type) {
@@ -656,7 +735,7 @@ export class StatementEmitter extends CpiEmitter {
656
735
  this.line(`}`);
657
736
  }
658
737
  else {
659
- const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
738
+ const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
660
739
  const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
661
740
  if (accId)
662
741
  this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
@@ -708,6 +787,17 @@ export class StatementEmitter extends CpiEmitter {
708
787
  }
709
788
  /** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
710
789
  closureReturn(stmt, frame) {
790
+ if (frame.into && stmt.expression) {
791
+ // Written straight into its place (an object literal for a struct, text for a FixedString, or a value).
792
+ const target = frame.into;
793
+ const vt = this.storageValueType(target.type);
794
+ if (vt)
795
+ this.line(this.writeStorage(target, this.ownedString(this.expect(this.expr(stmt.expression, vt), vt, stmt.expression)).c) + ";");
796
+ else
797
+ this.writeAggregate(target, stmt.expression);
798
+ this.line(`goto ${frame.label};`);
799
+ return;
800
+ }
711
801
  if (!stmt.expression) {
712
802
  if (frame.type)
713
803
  this.fail(stmt, "this function returns a value");
@@ -781,7 +871,7 @@ export class StatementEmitter extends CpiEmitter {
781
871
  c: "",
782
872
  type: BOOL,
783
873
  mutable: false,
784
- closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes] },
874
+ closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports },
785
875
  });
786
876
  continue;
787
877
  }
@@ -797,6 +887,15 @@ export class StatementEmitter extends CpiEmitter {
797
887
  this.fail(d.name, `"${name}" shadows the parameter "${name}"; use another name`);
798
888
  const c = `l_${name}_${this.temp++}`;
799
889
  const init = d.initializer;
890
+ // A function as a value: another name for one (`const h = g`), or the one a function returns (`adder(5n)`).
891
+ const made = ts.isCallExpression(init) ? this.madeFunction(init) : undefined;
892
+ const alias = ts.isIdentifier(init) ? this.functionNamed(init) : undefined;
893
+ if (made || alias) {
894
+ if (mutable)
895
+ this.fail(d, "bind functions with const");
896
+ this.scopes.at(-1).set(name, { c: "", type: BOOL, mutable: false, closure: (made ?? alias) });
897
+ continue;
898
+ }
800
899
  const fn = ts.isCallExpression(init) ? this.exportName(init.expression) : undefined;
801
900
  if (fn === "fixedArray") {
802
901
  const t = this.localArray(init, c);
@@ -1009,6 +1108,8 @@ export class StatementEmitter extends CpiEmitter {
1009
1108
  const pieces = this.templatePieces(rhsNode);
1010
1109
  if (pieces.text !== undefined)
1011
1110
  this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
1111
+ else if (pieces.only !== undefined)
1112
+ this.line(`TS_TRY(ts_str_append(&${cur}, ${pieces.only}));`);
1012
1113
  else
1013
1114
  this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
1014
1115
  return;
@@ -27,8 +27,12 @@ export declare abstract class StorageEmitter extends EmitterBase {
27
27
  * value. Refuse when a live reference into the replaced place is still used afterwards.
28
28
  */
29
29
  protected assertNotAliased(replaced: string[] | undefined, at: ts.Node, helper?: string): void;
30
- /** Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`). */
31
- protected usedAfter(decl: ts.VariableDeclaration, at: ts.Node): boolean;
30
+ /**
31
+ * Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
32
+ * A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
33
+ * `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
34
+ */
35
+ protected usedAfter(decl: ts.VariableDeclaration | ts.ParameterDeclaration, at: ts.Node): boolean;
32
36
  /**
33
37
  * A pointer to a struct in storage, from `place` or `cond ? placeA : placeB` (same struct type).
34
38
  * Returns undefined when the expression is not such a place.
@@ -73,16 +73,7 @@ export class StorageEmitter extends EmitterBase {
73
73
  return r?.type.kind === "dataEnum" ? r : undefined;
74
74
  }
75
75
  looksLikeDataEnum(node) {
76
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
77
- try {
78
- return this.reporter.guard(() => this.ref(node))?.type.kind === "dataEnum";
79
- }
80
- finally {
81
- this.lines.length = saved.lines;
82
- this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
83
- this.temp = saved.temp;
84
- this.reporter.diagnostics.length = saved.diagnostics;
85
- }
76
+ return this.probe(() => this.ref(node))?.type.kind === "dataEnum";
86
77
  }
87
78
  variantIndex(enumName, lit) {
88
79
  const e = this.model.dataEnums.get(enumName);
@@ -137,11 +128,37 @@ export class StorageEmitter extends EmitterBase {
137
128
  : `"${name}" still refers to ${hit}; replacing it here would change what "${name}" reads (in JS it keeps the old value). Copy the fields you need into locals first`);
138
129
  }
139
130
  }
140
- /** Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`). */
131
+ /**
132
+ * Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
133
+ * A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
134
+ * `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
135
+ */
141
136
  usedAfter(decl, at) {
142
137
  const symbol = this.checker.getSymbolAtLocation(decl.name);
143
138
  if (!symbol)
144
139
  return true;
140
+ if (ts.isParameter(decl)) {
141
+ const fn = decl.parent;
142
+ const inside = at.getSourceFile() === fn.getSourceFile() && at.pos >= fn.pos && at.end <= fn.end;
143
+ let read = false;
144
+ const visitParam = (n) => {
145
+ if (read)
146
+ return;
147
+ if (ts.isIdentifier(n) && n !== decl.name && this.checker.getSymbolAtLocation(n) === symbol) {
148
+ if (!inside || n.pos >= at.end)
149
+ read = true;
150
+ // A loop inside the function, around `at`, reads it again on its next turn.
151
+ for (let p = at.parent; inside && p && p !== fn; p = p.parent)
152
+ if ((ts.isForStatement(p) || ts.isForOfStatement(p) || ts.isWhileStatement(p) || ts.isDoStatement(p)) && n.pos >= p.pos && n.end <= p.end)
153
+ read = true;
154
+ }
155
+ ts.forEachChild(n, visitParam);
156
+ };
157
+ const body = fn.body;
158
+ if (body)
159
+ visitParam(body);
160
+ return read;
161
+ }
145
162
  // Loops around `at` that do not also contain the declaration run their earlier statements again later.
146
163
  let loop;
147
164
  for (let n = at.parent; n && n !== decl.parent; n = n.parent)
@@ -208,17 +225,8 @@ export class StorageEmitter extends EmitterBase {
208
225
  return this.looksLikeStructPlace(node.expression);
209
226
  if (ts.isConditionalExpression(node))
210
227
  return this.looksLikeStructPlace(node.whenTrue) && this.looksLikeStructPlace(node.whenFalse);
211
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
212
- try {
213
- const r = this.reporter.guard(() => this.ref(node));
214
- return r?.type.kind === "struct";
215
- }
216
- finally {
217
- this.lines.length = saved.lines;
218
- this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
219
- this.temp = saved.temp;
220
- this.reporter.diagnostics.length = saved.diagnostics;
221
- }
228
+ const r = this.probe(() => this.ref(node));
229
+ return r?.type.kind === "struct";
222
230
  }
223
231
  /** `fixedArray<T, N>(fill)` as a local declaration. */
224
232
  localArray(init, c) {
@@ -591,6 +599,11 @@ export class StorageEmitter extends EmitterBase {
591
599
  const els = valueNode.elements;
592
600
  if (els.length > t.cap)
593
601
  this.fail(valueNode, `${els.length} elements do not fit in ${typeName(t)}`);
602
+ // Empty: nothing reads the old list, so it is cleared in place (length 0, every slot zero).
603
+ if (els.length === 0) {
604
+ this.line(`ts_memzero(&${target.lv}, sizeof(${target.lv}));`);
605
+ return;
606
+ }
594
607
  const tmp = this.tmp("fv");
595
608
  this.line(`${fixedVecC(t)} ${tmp};`);
596
609
  this.line(`ts_memzero(&${tmp}, sizeof(${tmp}));`);
@@ -681,16 +694,7 @@ export class StorageEmitter extends EmitterBase {
681
694
  }
682
695
  /** The kind of storage an expression names (struct, array, dataEnum…), without emitting code or failing. */
683
696
  peekKind(node) {
684
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
685
- try {
686
- return this.reporter.guard(() => this.ref(node))?.type.kind;
687
- }
688
- finally {
689
- this.lines.length = saved.lines;
690
- this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
691
- this.temp = saved.temp;
692
- this.reporter.diagnostics.length = saved.diagnostics;
693
- }
697
+ return this.probe(() => this.ref(node))?.type.kind;
694
698
  }
695
699
  place(node) {
696
700
  if (ts.isIdentifier(node)) {
@@ -423,6 +423,14 @@ export interface ProgramModel {
423
423
  helperDecls: Map<ts.Node, string>;
424
424
  /** Local closures (they read the enclosing function's variables): their declaration -> name; expanded where called. */
425
425
  closureDecls: Map<ts.Node, string>;
426
+ /**
427
+ * Functions that return a function (`function adder(n: u64): (x: u64) => u64 { return (x) => x + n; }`): no C
428
+ * function; a call binds the returned function, with the arguments, as a closure where it is made.
429
+ */
430
+ factories: Map<string, {
431
+ node: HelperNode;
432
+ imports: Map<string, string>;
433
+ }>;
426
434
  errors: ErrorDef[];
427
435
  /** Enum name -> member name -> error */
428
436
  errorEnums: Map<string, Map<string, ErrorDef>>;
@@ -25,7 +25,12 @@ export interface PublishOptions {
25
25
  /** Write (create or update) the IDL metadata account; returns its address. */
26
26
  export declare function publishIdl(chain: Chain, opts: PublishOptions): Promise<Address>;
27
27
  export declare function metadataAddress(program: Address, authority: Address | null): Promise<Address>;
28
- /** Read an IDL back (canonical, or the one `authority` wrote). */
28
+ /** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
29
+ export declare const MAX_IDL_BYTES: number;
30
+ /**
31
+ * Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
32
+ * read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
33
+ */
29
34
  export declare function fetchIdl(rpc: Chain["rpc"], program: Address, authority?: Address | null): Promise<object>;
30
35
  /** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
31
36
  export declare function anchorIdlAddress(program: Address): Promise<Address>;
@@ -5,8 +5,9 @@
5
5
  *
6
6
  * Works against an RPC (`rpcChain`) or anything else that can read accounts and send transactions (tests use LiteSVM).
7
7
  */
8
- import { inflateSync } from "node:zlib";
9
- import { Compression, Encoding, Format, fetchMetadataContent, findMetadataPda, packDirectData, writeMetadata } from "@solana-program/program-metadata";
8
+ import { createHash } from "node:crypto";
9
+ import { gunzipSync, inflateSync } from "node:zlib";
10
+ import { Compression, DataSource, Encoding, Format, decodeData, fetchMaybeMetadata, findMetadataPda, packDirectData, writeMetadata, } from "@solana-program/program-metadata";
10
11
  import { createAddressWithSeed, createClient, createTransactionMessage, createTransactionPlanExecutor, createTransactionPlanner, extendClient, getAddressEncoder, getBase64EncodedWireTransaction, getProgramDerivedAddress, getSignatureFromTransaction, lamports, pipe, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, signTransactionMessageWithSigners, } from "@solana/kit";
11
12
  export const PROGRAM_METADATA_ID = "ProgM6JCCvbYkfKqJYHePx4xxSUSqJp7rh8Lyv7nk7S";
12
13
  /** A Chain over an RPC endpoint (polls signature status: no websocket needed). */
@@ -111,15 +112,54 @@ function planFailure(e) {
111
112
  export async function metadataAddress(program, authority) {
112
113
  return (await findMetadataPda({ program, authority, seed: "idl" }))[0];
113
114
  }
114
- /** Read an IDL back (canonical, or the one `authority` wrote). */
115
+ /** The most an IDL read from the chain may inflate to: compressed data can expand far past the account's size. */
116
+ export const MAX_IDL_BYTES = 16 * 1024 * 1024;
117
+ /** Decompress `data` (zlib or gzip), refusing output past MAX_IDL_BYTES. */
118
+ function inflateCapped(data, gzip, what) {
119
+ try {
120
+ return (gzip ? gunzipSync : inflateSync)(data, { maxOutputLength: MAX_IDL_BYTES });
121
+ }
122
+ catch (e) {
123
+ if (e.code === "ERR_BUFFER_TOO_LARGE" || e instanceof RangeError)
124
+ throw new Error(`${what} inflates to more than ${MAX_IDL_BYTES / 1024 / 1024} MiB; refusing it`);
125
+ throw new Error(`${what} is not valid compressed data: ${e.message}`);
126
+ }
127
+ }
128
+ /**
129
+ * Read an IDL back (canonical, or the one `authority` wrote). Only an IDL stored in the metadata account itself is
130
+ * read: one that links to a URL or points at another account is refused (nothing is fetched from where it points).
131
+ */
115
132
  export async function fetchIdl(rpc, program, authority = null) {
116
- return JSON.parse(await fetchMetadataContent(rpc, program, "idl", authority));
133
+ const at = await metadataAddress(program, authority);
134
+ const account = await fetchMaybeMetadata(rpc, at);
135
+ if (!account.exists)
136
+ throw new Error(`no IDL metadata account at ${at}`);
137
+ const m = account.data;
138
+ if (m.dataLength > m.data.length)
139
+ throw new Error(`the IDL metadata at ${at} is truncated`);
140
+ const content = (what) => {
141
+ const raw = new Uint8Array(m.data.subarray(0, m.dataLength));
142
+ const bytes = m.compression === Compression.None ? raw : inflateCapped(raw, m.compression === Compression.Gzip, what);
143
+ // Base58 decodes in quadratic time: only small contents.
144
+ if (m.encoding === Encoding.Base58 && bytes.length > 64 * 1024)
145
+ throw new Error(`${what} is too large for base58`);
146
+ return decodeData(bytes, m.encoding);
147
+ };
148
+ if (m.dataSource === DataSource.Url) {
149
+ const url = content(`the IDL link at ${at}`);
150
+ throw new Error(`the IDL at ${at} is not stored on chain but linked: ${JSON.stringify(url.slice(0, 200))}. Fetch it yourself and pass the file (typesol client <idl.json>)`);
151
+ }
152
+ if (m.dataSource !== DataSource.Direct)
153
+ throw new Error(`the IDL at ${at} is stored in another account (external data), which is not read`);
154
+ return JSON.parse(content(`the IDL at ${at}`));
117
155
  }
118
156
  /** Where `anchor idl init` keeps a program's IDL: an account derived with seed "anchor:idl" from the program's base PDA. */
119
157
  export async function anchorIdlAddress(program) {
120
158
  const [base] = await getProgramDerivedAddress({ programAddress: program, seeds: [] });
121
159
  return createAddressWithSeed({ baseAddress: base, programAddress: program, seed: "anchor:idl" });
122
160
  }
161
+ /** Anchor's account discriminator for IdlAccount: sha256("account:IdlAccount")[0..8]. */
162
+ const IDL_ACCOUNT_DISCRIMINATOR = createHash("sha256").update("account:IdlAccount").digest().subarray(0, 8);
123
163
  /** The IDL in Anchor's IDL account (`anchor idl init`), or null when the program has none. */
124
164
  export async function fetchAnchorIdl(rpc, program) {
125
165
  const { value } = await rpc.getAccountInfo(await anchorIdlAddress(program), { encoding: "base64" }).send();
@@ -129,10 +169,12 @@ export async function fetchAnchorIdl(rpc, program) {
129
169
  // IdlAccount: 8-byte discriminator, authority (32 bytes), a u32 length, then the zlib-compressed JSON.
130
170
  if (data.length < 44)
131
171
  throw new Error("Anchor IDL account is too short");
172
+ if (!data.subarray(0, 8).equals(IDL_ACCOUNT_DISCRIMINATOR))
173
+ throw new Error("the account at Anchor's IDL address is not an IdlAccount");
132
174
  const len = data.readUInt32LE(40);
133
175
  if (44 + len > data.length)
134
176
  throw new Error("Anchor IDL account is truncated");
135
- return JSON.parse(inflateSync(data.subarray(44, 44 + len)).toString("utf8"));
177
+ return JSON.parse(inflateCapped(data.subarray(44, 44 + len), false, "the Anchor IDL").toString("utf8"));
136
178
  }
137
179
  /**
138
180
  * A program's IDL from the chain: the canonical Program Metadata IDL (`typesol idl publish`, Codama) first, else
package/dist/portgen.js CHANGED
@@ -152,7 +152,7 @@ export function generatePort(idl) {
152
152
  if (t.vec) {
153
153
  const inner = tsType(t.vec, where, at);
154
154
  if (where === "variant" || where === "struct" || (where === "account" && !last)) {
155
- notes.push(`${at}: a Vec here (TypeSOL keeps a Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
155
+ notes.push(`${at}: a Vec here (TypeSOL keeps Anchor's bytes for a growable Vec only as an account's last field or as an argument); replaced by FixedArray, so the layout differs.`);
156
156
  return `${use("FixedArray")}<${inner}, 8> /* TODO: Anchor has a Vec here */`;
157
157
  }
158
158
  return `${use("Vec")}<${inner}>`;
package/dist/svmrpc.d.ts CHANGED
@@ -8,7 +8,11 @@
8
8
  * await counter.accounts.Counter.fetch(rpc, address);
9
9
  *
10
10
  * Transactions run as they are sent (confirmed at once). LiteSVM cannot list accounts, so `getProgramAccounts` scans
11
- * the accounts transactions sent through this connection named, plus those passed to `track`.
11
+ * the accounts transactions sent through this connection named, plus those passed to `track`. Subscriptions see the
12
+ * transactions sent through this connection: `logsNotifications` replays those sent so far (failed ones too, with
13
+ * `err` set), then each new one, as `{ mentions: [address] }` filters them; `accountNotifications` yields the account
14
+ * after each later transaction that names it. Errors are in the JSON shape RPC nodes return
15
+ * (`{ InstructionError: [0, { Custom: 6000 }] }`).
12
16
  */
13
17
  import { type Address, type Rpc, type RpcSubscriptions, type SolanaRpcApi, type SolanaRpcSubscriptionsApi } from "@solana/kit";
14
18
  import { type LiteSVM, type TransactionMetadata } from "litesvm";
@@ -23,4 +27,6 @@ export interface SvmConnection {
23
27
  /** Add accounts for `getProgramAccounts` to scan (accounts the transactions named are scanned already). */
24
28
  track(...addresses: Address[]): void;
25
29
  }
30
+ /** A LiteSVM transaction error in the JSON shape RPC nodes return: `{ InstructionError: [0, { Custom: 6000 }] }`. */
31
+ export declare function rpcError(err: unknown): unknown;
26
32
  export declare function svmConnection(svm: LiteSVM): SvmConnection;