@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.
@@ -1526,6 +1526,8 @@ export class CpiEmitter extends CallEmitter {
1526
1526
  this.line(`static const uint8_t ${name}[${f.bytes.length}] = ${cBytes(f.bytes)};`);
1527
1527
  put(name, String(f.bytes.length), f.bytes.length);
1528
1528
  }
1529
+ else if ("c" in f)
1530
+ put(f.c, f.n);
1529
1531
  else
1530
1532
  this.serialize(this.expect(this.expr(f.value, f.type), f.type, f.value), f.value, put);
1531
1533
  }
@@ -1829,8 +1831,237 @@ export class CpiEmitter extends CallEmitter {
1829
1831
  this.cpi(p, metas, data.buf, data.len, seedsAt(4));
1830
1832
  return undefined;
1831
1833
  }
1834
+ case "addExternalPlugin":
1835
+ case "removeExternalPlugin":
1836
+ case "writeExternalPluginData": {
1837
+ // Add/RemoveExternalPluginAdapterV1: asset, collection, payer, authority, systemProgram, logWrapper;
1838
+ // WriteExternalPluginAdapterDataV1 has a buffer account (left out: the data is in the instruction) before systemProgram.
1839
+ const write = method === "writeExternalPluginData";
1840
+ const n = write ? 7 : 6;
1841
+ if (a.length !== n && a.length !== n + 1)
1842
+ this.fail(node, `${method}(asset, collection | null, payer, authority | null, systemProgram, ${method === "addExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
1843
+ const metas = [
1844
+ this.mplMeta(a[0], core, "the asset", true, false),
1845
+ this.mplMeta(a[1], core, "the collection", true, false),
1846
+ this.mplMeta(a[2], core, "the payer", true, true),
1847
+ this.mplMeta(a[3], core, "the authority", false, true),
1848
+ ...(write ? [self] : []),
1849
+ this.mplMeta(a[4], core, "the system program", false, false),
1850
+ self,
1851
+ ];
1852
+ const body = method === "addExternalPlugin"
1853
+ ? this.coreExternalPlugin(a[5])
1854
+ : write
1855
+ ? [...this.coreExternalKey(a[5]), ...this.coreExternalData(a[6])]
1856
+ : this.coreExternalKey(a[5]);
1857
+ const data = this.mplData(node, 1024, [{ bytes: [{ addExternalPlugin: 22, removeExternalPlugin: 24, writeExternalPluginData: 28 }[method]] }, ...body]);
1858
+ this.cpi(p, metas, data.buf, data.len, seedsAt(n));
1859
+ return undefined;
1860
+ }
1861
+ case "addCollectionExternalPlugin":
1862
+ case "removeCollectionExternalPlugin":
1863
+ case "writeCollectionExternalPluginData": {
1864
+ // The same on a collection: collection, payer, authority, (buffer,) systemProgram, logWrapper.
1865
+ const write = method === "writeCollectionExternalPluginData";
1866
+ const n = write ? 6 : 5;
1867
+ if (a.length !== n && a.length !== n + 1)
1868
+ this.fail(node, `${method}(collection, payer, authority | null, systemProgram, ${method === "addCollectionExternalPlugin" ? "plugin" : write ? "key, data" : "key"}, signerSeeds?)`);
1869
+ const metas = [
1870
+ this.mplMeta(a[0], core, "the collection", true, false),
1871
+ this.mplMeta(a[1], core, "the payer", true, true),
1872
+ this.mplMeta(a[2], core, "the authority", false, true),
1873
+ ...(write ? [self] : []),
1874
+ this.mplMeta(a[3], core, "the system program", false, false),
1875
+ self,
1876
+ ];
1877
+ const body = method === "addCollectionExternalPlugin"
1878
+ ? this.coreExternalPlugin(a[4])
1879
+ : write
1880
+ ? [...this.coreExternalKey(a[4]), ...this.coreExternalData(a[5])]
1881
+ : this.coreExternalKey(a[4]);
1882
+ const data = this.mplData(node, 1024, [
1883
+ { bytes: [{ addCollectionExternalPlugin: 23, removeCollectionExternalPlugin: 25, writeCollectionExternalPluginData: 29 }[method]] },
1884
+ ...body,
1885
+ ]);
1886
+ this.cpi(p, metas, data.buf, data.len, seedsAt(n));
1887
+ return undefined;
1888
+ }
1889
+ }
1890
+ this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin, addExternalPlugin, removeExternalPlugin, writeExternalPluginData (and the Collection forms), transferAsset and burnAsset, not ${method}`);
1891
+ }
1892
+ /** The fields of a Core object literal (`{ kind: "AppData", … }`) by name. */
1893
+ coreProps(node, what) {
1894
+ let n = node;
1895
+ while (ts.isParenthesizedExpression(n) || ts.isAsExpression(n) || ts.isSatisfiesExpression(n))
1896
+ n = n.expression;
1897
+ if (!ts.isObjectLiteralExpression(n))
1898
+ this.fail(n, `${what} is an object literal: { kind: "…", … }`);
1899
+ const props = new Map();
1900
+ for (const q of n.properties) {
1901
+ if (ts.isPropertyAssignment(q) && ts.isIdentifier(q.name))
1902
+ props.set(q.name.text, q.initializer);
1903
+ else if (ts.isShorthandPropertyAssignment(q))
1904
+ props.set(q.name.text, q.name);
1905
+ else
1906
+ this.fail(q, "write the fields as name: value");
1907
+ }
1908
+ return props;
1909
+ }
1910
+ /** PluginAuthority: "none" | "owner" | "updateAuthority" (literals), or an address. */
1911
+ coreAuthority(n) {
1912
+ if (ts.isStringLiteral(n)) {
1913
+ const i = ["none", "owner", "updateAuthority"].indexOf(n.text);
1914
+ if (i < 0)
1915
+ this.fail(n, 'an authority is "none", "owner", "updateAuthority" or a Pubkey');
1916
+ return [{ bytes: [i] }];
1917
+ }
1918
+ return [{ bytes: [3] }, { value: n, type: PUBKEY }];
1919
+ }
1920
+ coreOption(n, inner) {
1921
+ return n ? [{ bytes: [1] }, ...inner(n)] : [{ bytes: [0] }];
1922
+ }
1923
+ coreSchema(n) {
1924
+ const i = ts.isStringLiteral(n) ? ["binary", "json", "msgpack"].indexOf(n.text) : -1;
1925
+ if (i < 0)
1926
+ this.fail(n, 'schema is "binary", "json" or "msgpack"');
1927
+ return [{ bytes: [i] }];
1928
+ }
1929
+ /** Vec<(HookableLifecycleEvent, ExternalCheckResult)>: `[{ event: "transfer", canReject: true }, …]`. */
1930
+ coreChecks(n, at) {
1931
+ if (!n || !ts.isArrayLiteralExpression(n))
1932
+ this.fail(n ?? at, 'checks is a list: [{ event: "transfer", canApprove: true, canReject: true }]');
1933
+ const out = [{ bytes: [n.elements.length & 0xff, 0, 0, 0] }];
1934
+ for (const e of n.elements) {
1935
+ const p = this.coreProps(e, "a lifecycle check");
1936
+ const ev = p.get("event");
1937
+ const i = ev && ts.isStringLiteral(ev) ? ["create", "transfer", "burn", "update", "execute"].indexOf(ev.text) : -1;
1938
+ if (i < 0)
1939
+ this.fail(ev ?? e, 'event is "create", "transfer", "burn", "update" or "execute"');
1940
+ let flags = 0;
1941
+ for (const [k, bit] of [
1942
+ ["canListen", 1],
1943
+ ["canApprove", 2],
1944
+ ["canReject", 4],
1945
+ ]) {
1946
+ const v = p.get(k);
1947
+ if (v && v.kind !== ts.SyntaxKind.TrueKeyword && v.kind !== ts.SyntaxKind.FalseKeyword)
1948
+ this.fail(v, `${k} is true or false`);
1949
+ if (v?.kind === ts.SyntaxKind.TrueKeyword)
1950
+ flags |= bit;
1951
+ }
1952
+ for (const k of p.keys())
1953
+ if (!["event", "canListen", "canApprove", "canReject"].includes(k))
1954
+ this.fail(e, `a lifecycle check has event, canListen, canApprove and canReject, not ${k}`);
1955
+ out.push({ bytes: [i, flags, 0, 0, 0] });
1956
+ }
1957
+ return out;
1958
+ }
1959
+ /** ExtraAccount: `{ kind: "address", address, isSigner?, isWritable? }` or a preconfigured one (`{ kind: "owner" }`, …). */
1960
+ coreExtraAccount(n) {
1961
+ const p = this.coreProps(n, "an extra account");
1962
+ const kind = p.get("kind");
1963
+ const kinds = ["program", "collection", "owner", "recipient", "asset", "customPda", "address"];
1964
+ const i = kind && ts.isStringLiteral(kind) ? kinds.indexOf(kind.text) : -1;
1965
+ if (i < 0 || i === 5)
1966
+ this.fail(kind ?? n, 'an extra account is { kind: "address", address } or "program", "collection", "owner", "recipient", "asset"');
1967
+ const flag = (k) => {
1968
+ const v = p.get(k);
1969
+ if (v && v.kind !== ts.SyntaxKind.TrueKeyword && v.kind !== ts.SyntaxKind.FalseKeyword)
1970
+ this.fail(v, `${k} is true or false`);
1971
+ return v?.kind === ts.SyntaxKind.TrueKeyword ? 1 : 0;
1972
+ };
1973
+ if (i === 6) {
1974
+ const addr = p.get("address");
1975
+ if (!addr)
1976
+ this.fail(n, "an address extra account needs address");
1977
+ return [{ bytes: [6] }, { value: addr, type: PUBKEY }, { bytes: [flag("isSigner"), flag("isWritable")] }];
1978
+ }
1979
+ return [{ bytes: [i, flag("isSigner"), flag("isWritable")] }];
1980
+ }
1981
+ /**
1982
+ * ExternalPluginAdapterInitInfo from a literal: `{ kind: "AppData", dataAuthority, schema? }`, `{ kind: "Oracle",
1983
+ * baseAddress, checks, resultsOffset?, baseAddressConfig? }`, `{ kind: "LifecycleHook", hookedProgram, checks,
1984
+ * extraAccounts?, dataAuthority?, schema? }` and the Linked forms (collections); `authority?` is the plugin's own.
1985
+ */
1986
+ coreExternalPlugin(n) {
1987
+ const p = this.coreProps(n, "a Core external plugin");
1988
+ const kind = p.get("kind");
1989
+ const kinds = ["LifecycleHook", "Oracle", "AppData", "LinkedLifecycleHook", "LinkedAppData"];
1990
+ const i = kind && ts.isStringLiteral(kind) ? kinds.indexOf(kind.text) : -1;
1991
+ if (i < 0)
1992
+ this.fail(kind ?? n, `a Core external plugin is ${kinds.join(", ")}`);
1993
+ const allowed = {
1994
+ 0: ["hookedProgram", "checks", "extraAccounts", "dataAuthority", "schema"],
1995
+ 1: ["baseAddress", "checks", "baseAddressConfig", "resultsOffset"],
1996
+ 2: ["dataAuthority", "schema"],
1997
+ 3: ["hookedProgram", "checks", "extraAccounts", "dataAuthority", "schema"],
1998
+ 4: ["dataAuthority", "schema"],
1999
+ };
2000
+ for (const k of p.keys())
2001
+ if (k !== "kind" && k !== "authority" && !allowed[i].includes(k))
2002
+ this.fail(n, `${kinds[i]} takes ${allowed[i].join(", ")} and authority, not ${k}`);
2003
+ const need = (k) => p.get(k) ?? this.fail(n, `${kinds[i]} needs ${k}`);
2004
+ const auth = this.coreOption(p.get("authority"), (x) => this.coreAuthority(x));
2005
+ const schema = this.coreOption(p.get("schema"), (x) => this.coreSchema(x));
2006
+ if (i === 2 || i === 4)
2007
+ return [{ bytes: [i] }, ...this.coreAuthority(need("dataAuthority")), ...auth, ...schema];
2008
+ if (i === 1) {
2009
+ const offset = p.get("resultsOffset");
2010
+ const off = !offset
2011
+ ? [{ bytes: [0] }]
2012
+ : ts.isStringLiteral(offset) && (offset.text === "none" || offset.text === "anchor")
2013
+ ? [{ bytes: [1, offset.text === "none" ? 0 : 1] }]
2014
+ : [{ bytes: [1, 2] }, { value: offset, type: U64 }];
2015
+ return [
2016
+ { bytes: [1] },
2017
+ { value: need("baseAddress"), type: PUBKEY },
2018
+ ...auth,
2019
+ ...this.coreChecks(need("checks"), n),
2020
+ ...this.coreOption(p.get("baseAddressConfig"), (x) => this.coreExtraAccount(x)),
2021
+ ...off,
2022
+ ];
2023
+ }
2024
+ const extras = p.get("extraAccounts");
2025
+ if (extras && !ts.isArrayLiteralExpression(extras))
2026
+ this.fail(extras, "extraAccounts is a list of extra accounts");
2027
+ return [
2028
+ { bytes: [i] },
2029
+ { value: need("hookedProgram"), type: PUBKEY },
2030
+ ...auth,
2031
+ ...this.coreChecks(need("checks"), n),
2032
+ ...(extras ? [{ bytes: [1, extras.elements.length & 0xff, 0, 0, 0] }, ...extras.elements.flatMap((e) => this.coreExtraAccount(e))] : [{ bytes: [0] }]),
2033
+ ...this.coreOption(p.get("dataAuthority"), (x) => this.coreAuthority(x)),
2034
+ ...schema,
2035
+ ];
2036
+ }
2037
+ /** ExternalPluginAdapterKey: `{ kind: "AppData", dataAuthority }`, `{ kind: "Oracle", address }`, `{ kind: "LifecycleHook", address }`, … */
2038
+ coreExternalKey(n) {
2039
+ const p = this.coreProps(n, "a Core external plugin key");
2040
+ const kind = p.get("kind");
2041
+ const kinds = ["LifecycleHook", "Oracle", "AppData", "LinkedLifecycleHook", "LinkedAppData"];
2042
+ const i = kind && ts.isStringLiteral(kind) ? kinds.indexOf(kind.text) : -1;
2043
+ if (i < 0)
2044
+ this.fail(kind ?? n, `a Core external plugin key is ${kinds.join(", ")}`);
2045
+ if (i === 2 || i === 4)
2046
+ return [{ bytes: [i] }, ...this.coreAuthority(p.get("dataAuthority") ?? this.fail(n, `${kinds[i]}'s key is its dataAuthority`))];
2047
+ return [
2048
+ { bytes: [i] },
2049
+ { value: p.get("address") ?? this.fail(n, `${kinds[i]}'s key is its address (the oracle's base address or the hooked program)`), type: PUBKEY },
2050
+ ];
2051
+ }
2052
+ /** The data written: Option<Vec<u8>> from a string, a Vec<u8> argument, or null (clears it). */
2053
+ coreExternalData(n) {
2054
+ if (n.kind === ts.SyntaxKind.NullKeyword)
2055
+ return [{ bytes: [0] }];
2056
+ const l = ts.isIdentifier(n) ? this.lookup(n.text) : undefined;
2057
+ if (l?.type.kind === "vecArg") {
2058
+ if (l.type.elem.kind !== "int" || l.type.elem.name !== "u8")
2059
+ this.fail(n, "data is a string, a Vec<u8> argument or null");
2060
+ const t = this.tmp("dl");
2061
+ this.line(`uint32_t ${t} = (uint32_t)${l.c}.len;`);
2062
+ return [{ bytes: [1] }, { c: `&${t}`, n: "4" }, { c: `${l.c}.ptr`, n: `(uint64_t)${t}` }];
1832
2063
  }
1833
- this.fail(node.expression, `Metaplex Core supports createAsset, createCollection, addPlugin, updatePlugin, removePlugin (and the Collection forms), transferAsset and burnAsset, not ${method}`);
2064
+ return [{ bytes: [1] }, { value: n, type: STRING }];
1834
2065
  }
1835
2066
  /** PluginType (u8) from a kind string literal: removePlugin(…, "FreezeDelegate"). */
1836
2067
  corePluginType(n) {
@@ -32,17 +32,25 @@ export declare abstract class ExpressionEmitter extends StorageEmitter {
32
32
  text: string;
33
33
  arr?: undefined;
34
34
  n?: undefined;
35
+ only?: undefined;
35
36
  } | {
36
37
  text?: undefined;
37
38
  arr: string;
38
39
  n: number;
40
+ only?: undefined;
41
+ } | {
42
+ only: string;
43
+ text?: undefined;
39
44
  };
40
45
  /**
41
46
  * The text of `${e}` when it is known at compile time: a string literal or constant, a non-negative integer
42
47
  * literal, a boolean literal, or a Pubkey constant (its base58). Undefined otherwise.
43
48
  */
44
49
  protected constantText(e: ts.Expression): string | undefined;
45
- protected toText(node: ts.Expression): string;
50
+ /** `${node}`'s text; `converted.yes` when it was made from a non-string value (new text, not a view of other text). */
51
+ protected toText(node: ts.Expression, converted?: {
52
+ yes: boolean;
53
+ }): string;
46
54
  /** C statement writing the decimal text of an integer into the TS_Str `out`. */
47
55
  protected textOfInt(name: IntName, c: string, out: string): string;
48
56
  /** `c ? a : b` on values: only the chosen branch runs (its checks and side effects included). */
@@ -128,7 +128,7 @@ export class ExpressionEmitter extends StorageEmitter {
128
128
  if (ts.isNonNullExpression(node)) {
129
129
  const v = this.expr(node.expression, hint && hint.kind !== "option" ? { kind: "option", inner: hint } : hint);
130
130
  if (v.type.kind !== "option")
131
- this.fail(node, "! works on Option values; check optional accounts with `x !== null`");
131
+ this.fail(node, "! works on Option values and optional accounts (acct!.field); check other values with `x !== null`");
132
132
  return this.unwrap(v, node);
133
133
  }
134
134
  if (ts.isPropertyAccessExpression(node) && node.questionDotToken)
@@ -154,6 +154,8 @@ export class ExpressionEmitter extends StorageEmitter {
154
154
  if (ts.isTemplateExpression(node)) {
155
155
  // `a${x}b${y}c`: one join on the heap (or a literal when every piece is known at compile time).
156
156
  const pieces = this.templatePieces(node);
157
+ if (pieces.only !== undefined)
158
+ return { c: pieces.only, type: STRING };
157
159
  if (pieces.text !== undefined)
158
160
  return { c: `ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})`, type: STRING, stringLiteral: pieces.text };
159
161
  const t = this.tmp("tpl");
@@ -171,7 +173,7 @@ export class ExpressionEmitter extends StorageEmitter {
171
173
  const l = this.lookup(node.text);
172
174
  if (l) {
173
175
  if (l.closure)
174
- this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
176
+ this.fail(node, `"${node.text}" is a function: call it (${node.text}(…)), bind it with const, or pass it to a helper or list method; it cannot be stored or returned as a value`);
175
177
  if (l.type.kind === "array")
176
178
  this.fail(node, `"${node.text}" is an array; use .get(i)`);
177
179
  if (l.type.kind === "vec")
@@ -204,7 +206,7 @@ export class ExpressionEmitter extends StorageEmitter {
204
206
  if (this.accounts.has(node.text))
205
207
  this.fail(node, `"${node.text}" is an account; use ${node.text}.key for its address`);
206
208
  if (calleeName(node, this.model.helperDecls, this.checker) || this.model.helpers.has(node.text))
207
- this.fail(node, `"${node.text}" is a function: TypeSOL functions are called (${node.text}(…)), not passed around or stored`);
209
+ this.fail(node, `"${node.text}" is a function: call it (${node.text}(…)), bind it with const, or pass it to a helper or list method; it cannot be stored or returned as a value`);
208
210
  this.fail(node, `unknown name "${node.text}"`);
209
211
  }
210
212
  if (ts.isPropertyAccessExpression(node))
@@ -455,16 +457,20 @@ export class ExpressionEmitter extends StorageEmitter {
455
457
  lit(known);
456
458
  else {
457
459
  const mark = this.lines.length;
458
- const c = this.toText(span.expression);
460
+ const converted = { yes: false };
461
+ const c = this.toText(span.expression, converted);
459
462
  for (const p of parts)
460
463
  if (p.c !== undefined)
461
464
  p.c = this.settle({ c: p.c, type: STRING }, mark).c;
462
- parts.push({ c });
465
+ parts.push({ c, converted: converted.yes });
463
466
  }
464
467
  lit(span.literal.text);
465
468
  }
466
469
  if (parts.every((p) => p.text !== undefined))
467
470
  return { text: parts.map((p) => p.text).join("") };
471
+ // `${n}` alone: the text a conversion just made (or a constant one) is already a string of its own; no join.
472
+ if (parts.length === 1 && parts[0].converted)
473
+ return { only: parts[0].c };
468
474
  const strOf = (text) => `ts_str((const uint8_t *)${cString(text)}, ${Buffer.byteLength(text, "utf8")})`;
469
475
  const arr = this.tmp("parts");
470
476
  this.line(`TS_Str ${arr}[${parts.length}] = {${parts.map((p) => (p.text !== undefined ? strOf(p.text) : p.c)).join(", ")}};`);
@@ -494,10 +500,13 @@ export class ExpressionEmitter extends StorageEmitter {
494
500
  }
495
501
  return undefined;
496
502
  }
497
- toText(node) {
503
+ /** `${node}`'s text; `converted.yes` when it was made from a non-string value (new text, not a view of other text). */
504
+ toText(node, converted) {
498
505
  const v = this.expr(node);
499
506
  if (v.type.kind === "string")
500
507
  return v.c;
508
+ if (converted)
509
+ converted.yes = true;
501
510
  if (v.type.kind === "option" && (v.type.inner.kind === "int" || v.type.inner.kind === "bool")) {
502
511
  // `${opt}`: the value, or "null" like JS.
503
512
  const inner = v.type.inner;
@@ -557,12 +566,7 @@ export class ExpressionEmitter extends StorageEmitter {
557
566
  (ts.isBigIntLiteral(node.whenFalse) || ts.isPrefixUnaryExpression(node.whenFalse));
558
567
  if (!type && !bothLiteral && (ts.isBigIntLiteral(node.whenTrue) || node.whenTrue.kind === ts.SyntaxKind.NullKeyword)) {
559
568
  // Type the non-literal branch first, without emitting it, to give the literal its type.
560
- const saved = { lines: this.lines.length, hoisted: this.hoisted.length, temp: this.temp, diagnostics: this.reporter.diagnostics.length };
561
- const probe = this.reporter.guard(() => this.expr(node.whenFalse));
562
- this.lines.length = saved.lines;
563
- this.hoisted.length = saved.hoisted; // a probed helper call may hoist declarations
564
- this.temp = saved.temp;
565
- this.reporter.diagnostics.length = saved.diagnostics;
569
+ const probe = this.probe(() => this.expr(node.whenFalse));
566
570
  if (probe && probe.literal === undefined && !probe.isNull)
567
571
  type = probe.type;
568
572
  }
@@ -150,14 +150,16 @@ export class FunctionEmitter extends StatementEmitter {
150
150
  // The loader passes each account once and marks repeats as duplicates, which share its record (ts_deserialize),
151
151
  // so two parameters name the same account exactly when their pointers are equal.
152
152
  const writable = ix.accounts.filter((a) => isWritable(a.kind));
153
+ const pairs = [];
153
154
  for (let i = 0; i < writable.length; i++)
154
155
  for (let j = i + 1; j < writable.length; j++) {
155
156
  const present = [writable[i], writable[j]]
156
157
  .filter((a) => a.optional)
157
158
  .map((a) => `a_${a.name} && `)
158
159
  .join("");
159
- head.push(` if (${present}a_${writable[i].name} == a_${writable[j].name}) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: ${writable[i].name} and ${writable[j].name} must be different accounts");`);
160
+ pairs.push(`if (${present}a_${writable[i].name} == a_${writable[j].name}) TS_FAIL(TS_ERR_DUPLICATE_MUT, "TypeSOL: ${writable[i].name} and ${writable[j].name} must be different accounts");`);
160
161
  }
162
+ head.push(...pairs.map((p) => ` ${p}`));
161
163
  // Arguments: Borsh, read sequentially (strings make later offsets dynamic).
162
164
  if (ix.args.length)
163
165
  head.push(` uint64_t off = ${ix.discriminator.length};`);
@@ -75,7 +75,7 @@ export interface Local {
75
75
  readonly?: boolean;
76
76
  /** For struct / data-enum references: the storage paths they alias (e.g. "book.asks[]"), and the declaration. */
77
77
  paths?: string[];
78
- decl?: ts.VariableDeclaration;
78
+ decl?: ts.VariableDeclaration | ts.ParameterDeclaration;
79
79
  /** A local closure (`const f = (…) => …` reading the enclosing function's variables), expanded where it is called. */
80
80
  closure?: Closure;
81
81
  }
@@ -85,6 +85,16 @@ export interface Closure {
85
85
  node: ts.ArrowFunction | ts.FunctionExpression | ts.FunctionDeclaration;
86
86
  name: string;
87
87
  scopes: Map<string, Local>[];
88
+ /**
89
+ * The instruction arguments and accounts its body may name: those where it was written for a closure, none for a
90
+ * helper or a function a helper made (they see only their parameters and the module).
91
+ */
92
+ env: {
93
+ args: Map<string, Local>;
94
+ accounts: Map<string, AccountParam>;
95
+ };
96
+ /** The `typesol` imports of the file it is written in: its type names mean what they mean there. */
97
+ imports: Map<string, string>;
88
98
  }
89
99
  export declare const cBytes: (bytes: number[]) => string;
90
100
  /** C expressions that nothing in a TypeSOL expression can change: temporaries, locals, arguments, account keys, constants. */
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
  import { type FieldType } from "../model.js";
3
- import { type Value, type Closure } from "./shared.js";
3
+ import { type Value, type Closure, type Ref } from "./shared.js";
4
4
  import { CpiEmitter } from "./cpi.js";
5
5
  /**
6
6
  * Statements: variables, assignment, control flow and return.
@@ -18,6 +18,8 @@ export declare abstract class StatementEmitter extends CpiEmitter {
18
18
  * As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
19
19
  */
20
20
  protected forOf(stmt: ts.ForOfStatement): void;
21
+ /** The function an identifier names (a closure, a local function or a helper), as something to expand; else undefined. */
22
+ protected functionNamed(id: ts.Identifier): Closure | undefined;
21
23
  /** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
22
24
  protected isPipeline(e: ts.Expression): e is ts.CallExpression;
23
25
  /** The list under a chain of map / filter calls, and the calls in the order they run. */
@@ -65,7 +67,7 @@ export declare abstract class StatementEmitter extends CpiEmitter {
65
67
  * was declared with, so a block around the call that declares the same name does not change what it reads, and
66
68
  * writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
67
69
  */
68
- protected expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node): Value | undefined;
70
+ protected expandClosure(c: Closure, args: readonly ts.Expression[], node: ts.Node, into?: Ref): Value | undefined;
69
71
  /**
70
72
  * `xs.forEach / some / every / find / findIndex / reduce / includes / indexOf` on a Vec, FixedArray, FixedVec or Vec
71
73
  * argument: an index loop (the length read each step, as `for … of`), the callback (an arrow written at the call)