@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.
- package/CHANGELOG.md +61 -0
- package/README.md +13 -12
- package/SECURITY.md +1 -1
- package/dist/clientgen.js +190 -41
- package/dist/compiler/analyze.js +111 -19
- package/dist/compiler/emit/base.d.ts +17 -4
- package/dist/compiler/emit/base.js +60 -15
- package/dist/compiler/emit/calls.d.ts +7 -0
- package/dist/compiler/emit/calls.js +99 -9
- package/dist/compiler/emit/cpi.d.ts +23 -0
- package/dist/compiler/emit/cpi.js +232 -1
- package/dist/compiler/emit/expressions.d.ts +9 -1
- package/dist/compiler/emit/expressions.js +16 -12
- package/dist/compiler/emit/function.js +3 -1
- package/dist/compiler/emit/shared.d.ts +11 -1
- package/dist/compiler/emit/statements.d.ts +4 -2
- package/dist/compiler/emit/statements.js +128 -27
- package/dist/compiler/emit/storage.d.ts +6 -2
- package/dist/compiler/emit/storage.js +36 -32
- package/dist/compiler/model.d.ts +8 -0
- package/dist/idlpublish.d.ts +6 -1
- package/dist/idlpublish.js +47 -5
- package/dist/portgen.js +1 -1
- package/dist/svmrpc.d.ts +7 -1
- package/dist/svmrpc.js +133 -19
- package/dist/testing.js +23 -3
- package/docs/cli.md +2 -2
- package/docs/client.md +13 -10
- package/docs/errors.md +9 -3
- package/docs/language.md +18 -17
- package/docs/mainnet.md +1 -1
- package/docs/migrate-from-anchor.md +8 -9
- package/docs/test-your-program.md +6 -2
- package/docs/testing.md +1 -1
- package/docs/troubleshooting.md +3 -2
- package/lib/typesol.d.ts +120 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +54 -18
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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)
|