@typesol/cli 0.21.0 → 0.23.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 +43 -0
- package/README.md +1 -1
- package/dist/clientgen.js +357 -53
- package/dist/compiler/analyze.js +93 -15
- package/dist/compiler/emit/base.d.ts +11 -1
- package/dist/compiler/emit/base.js +21 -0
- package/dist/compiler/emit/calls.d.ts +11 -1
- package/dist/compiler/emit/calls.js +68 -6
- package/dist/compiler/emit/cpi.js +42 -4
- package/dist/compiler/emit/expressions.js +9 -0
- package/dist/compiler/emit/function.js +5 -3
- package/dist/compiler/emit/program.js +9 -2
- package/dist/compiler/emit/shared.d.ts +32 -2
- package/dist/compiler/emit/shared.js +43 -0
- package/dist/compiler/emit/statements.d.ts +35 -0
- package/dist/compiler/emit/statements.js +237 -79
- package/dist/compiler/emit/storage.js +39 -13
- package/dist/compiler/model.d.ts +18 -1
- package/dist/svmrpc.d.ts +26 -0
- package/dist/svmrpc.js +150 -0
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +3 -0
- package/docs/client.md +14 -12
- package/docs/language.md +9 -9
- package/docs/test-your-program.md +4 -1
- package/lib/typesol.d.ts +8 -0
- package/package.json +1 -1
- package/runtime/typesol_rt.h +19 -0
|
@@ -202,6 +202,49 @@ export function helperSignature(h) {
|
|
|
202
202
|
params.push(h.returns.kind === "struct" ? `TSS_${h.returns.name} *ret` : h.returns.kind === "string" ? `TS_Str *ret` : `${scalarC(h.returns)} *ret`);
|
|
203
203
|
return `static uint64_t fn_${h.name}(${params.join(", ")})`;
|
|
204
204
|
}
|
|
205
|
+
/** C type of a growable Vec stored after the first one of struct `s`: `{ uint32_t len; T items[]; }`. */
|
|
206
|
+
export function laterVecC(s, field) {
|
|
207
|
+
return `TSW_${s}__${field}`;
|
|
208
|
+
}
|
|
209
|
+
/** The account size of struct `s` with every growable Vec empty, as C (8-byte discriminator not included). */
|
|
210
|
+
export function emptySizeC(s) {
|
|
211
|
+
const n = s.vecs?.length ?? 0;
|
|
212
|
+
return n > 1 ? `(sizeof(TSS_${s.name}) + ${4 * (n - 1)})` : `sizeof(TSS_${s.name})`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Where growable Vec `k` of a struct at `base` (a C lvalue of type TSS_<S>) starts, as C: its length and elements.
|
|
216
|
+
* The first is a member of the struct; each further one starts right after the previous one's elements.
|
|
217
|
+
*/
|
|
218
|
+
export function vecPlace(s, base, k) {
|
|
219
|
+
const vs = s.vecs;
|
|
220
|
+
const first = vs[0];
|
|
221
|
+
let end = `((uint8_t *)${base}.${first.name} + (uint64_t)${base}.${first.name}__len * ${first.elemSize})`;
|
|
222
|
+
let place = { len: `${base}.${first.name}__len`, items: `${base}.${first.name}`, end };
|
|
223
|
+
for (let j = 1; j <= k; j++) {
|
|
224
|
+
const v = vs[j];
|
|
225
|
+
const h = `((${laterVecC(s.name, v.name)} *)${end})`;
|
|
226
|
+
end = `((uint8_t *)${h}->items + (uint64_t)${h}->len * ${v.elemSize})`;
|
|
227
|
+
place = { len: `${h}->len`, items: `${h}->items`, end };
|
|
228
|
+
}
|
|
229
|
+
return place;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* C lines checking that an account's size matches its growable Vecs' lengths (`d`: its TSS_<S> pointer, `a`: its
|
|
233
|
+
* TsAccount). One Vec: ts_check_vec_len; more: each length read only once the bytes before it are known to exist.
|
|
234
|
+
*/
|
|
235
|
+
export function vecLenCheck(s, a, d, pad = " ") {
|
|
236
|
+
const vs = s.vecs ?? [];
|
|
237
|
+
if (vs.length === 1)
|
|
238
|
+
return [`${pad}TS_TRY(ts_check_vec_len(${a}, sizeof(TSS_${s.name}), ${d}->${vs[0].name}__len, TS_ELEM_${s.name}));`];
|
|
239
|
+
const o = `${d.replace(/\W/g, "")}__vo`;
|
|
240
|
+
const lines = [`${pad}{`, `${pad} uint64_t ${o} = sizeof(TSS_${s.name}) + (uint64_t)${d}->${vs[0].name}__len * ${vs[0].elemSize};`];
|
|
241
|
+
for (const v of vs.slice(1)) {
|
|
242
|
+
lines.push(`${pad} if (${a}->data_len < 8 + ${o} + 4) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account size does not match its Vec lengths");`);
|
|
243
|
+
lines.push(`${pad} ${o} += 4 + (uint64_t)((${laterVecC(s.name, v.name)} *)(${a}->data + 8 + ${o}))->len * ${v.elemSize};`);
|
|
244
|
+
}
|
|
245
|
+
lines.push(`${pad} if (${a}->data_len != 8 + ${o}) TS_FAIL(TS_ERR_ACCOUNT_TOO_SMALL, "TypeSOL: account size does not match its Vec lengths");`, `${pad}}`);
|
|
246
|
+
return lines;
|
|
247
|
+
}
|
|
205
248
|
/** C typedef name of a FixedVec<T, N>: `{ uint32_t len; T items[N]; }`. */
|
|
206
249
|
export function fixedVecC(t) {
|
|
207
250
|
return `TSF_${fixedVecName(t)}`;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
+
import { type FieldType } from "../model.js";
|
|
2
3
|
import { type Value, type Closure } from "./shared.js";
|
|
3
4
|
import { CpiEmitter } from "./cpi.js";
|
|
4
5
|
/**
|
|
@@ -17,6 +18,40 @@ export declare abstract class StatementEmitter extends CpiEmitter {
|
|
|
17
18
|
* As in JS, the length is read again before every step, so a body that pushes or pops sees the new length.
|
|
18
19
|
*/
|
|
19
20
|
protected forOf(stmt: ts.ForOfStatement): void;
|
|
21
|
+
/** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
|
|
22
|
+
protected isPipeline(e: ts.Expression): e is ts.CallExpression;
|
|
23
|
+
/** The list under a chain of map / filter calls, and the calls in the order they run. */
|
|
24
|
+
protected pipelineStages(e: ts.Expression): {
|
|
25
|
+
base: ts.Expression;
|
|
26
|
+
stages: {
|
|
27
|
+
kind: "map" | "filter";
|
|
28
|
+
fn: ts.ArrowFunction | ts.FunctionExpression;
|
|
29
|
+
}[];
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Run `body` for each element of a list, or of a chain of `map` / `filter` calls on one (`xs.filter(f).map(g)`):
|
|
33
|
+
* one index loop over the list (its length read every step, as `for … of`), each callback expanded in it. A
|
|
34
|
+
* filter skips to the next element, a map binds its result, so a chain builds no list. `body` gets the element (as
|
|
35
|
+
* `list.get(i)` gives it, structs by reference, or the mapped value) and its index in the chain's output (after a
|
|
36
|
+
* filter, the count of elements it let through, as JS gives callbacks).
|
|
37
|
+
*/
|
|
38
|
+
protected eachElement(list: ts.Expression, at: ts.Node, names: {
|
|
39
|
+
index: string;
|
|
40
|
+
ci: string;
|
|
41
|
+
next?: string;
|
|
42
|
+
alwaysNext?: boolean;
|
|
43
|
+
}, body: (element: ts.Expression, index: ts.Expression) => void): void;
|
|
44
|
+
/**
|
|
45
|
+
* `const ys = xs.map(…)` / `xs.filter(…)`: the chain's elements in a local FixedVec, as big as the list it starts
|
|
46
|
+
* from (a FixedArray's length, a FixedVec's capacity) or as annotated (`const ys: FixedVec<u64, 16> = …`, needed
|
|
47
|
+
* when it starts from a Vec). It lives on the stack, so it holds at most 2 KiB; one more element than it holds aborts.
|
|
48
|
+
*/
|
|
49
|
+
protected localList(d: ts.VariableDeclaration, init: ts.CallExpression, c: string): FieldType & {
|
|
50
|
+
kind: "vec";
|
|
51
|
+
cap: number;
|
|
52
|
+
};
|
|
53
|
+
/** The element type of a Vec, FixedArray, FixedVec or Vec argument. */
|
|
54
|
+
protected listElem(list: ts.Expression): FieldType;
|
|
20
55
|
/** Whether `e` is the remaining accounts (or a const bound to them). */
|
|
21
56
|
protected isRemaining(e: ts.Expression): boolean;
|
|
22
57
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { MAX_RETURN_DATA, resolveValueType, structNameOf } from "../analyze.js";
|
|
2
|
+
import { MAX_RETURN_DATA, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
|
|
3
3
|
import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, cString, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
4
|
+
import { U64, BOOL, cString, fixedVecC, optC, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
5
5
|
import { CpiEmitter } from "./cpi.js";
|
|
6
6
|
/**
|
|
7
7
|
* Statements: variables, assignment, control flow and return.
|
|
@@ -259,37 +259,196 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
259
259
|
n.parent = stmt;
|
|
260
260
|
return n;
|
|
261
261
|
};
|
|
262
|
-
|
|
263
|
-
const length = place(f.createPropertyAccessExpression(stmt.expression, "length"));
|
|
264
|
-
const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(stmt.expression, "get")), undefined, [indexId]));
|
|
265
|
-
// The element's declaration: the source's own name (so aliasing checks find its uses) bound to list.get(i).
|
|
266
|
-
const decl = f.createVariableDeclaration(d.name, undefined, undefined, get);
|
|
267
|
-
ts.setTextRange(decl, d);
|
|
268
|
-
decl.parent = list;
|
|
269
|
-
const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
|
|
262
|
+
void place;
|
|
270
263
|
const ci = this.tmp("of_i");
|
|
271
264
|
const label = this.tmp("ts_continue_");
|
|
265
|
+
this.eachElement(stmt.expression, stmt, { index, ci, next: label, alwaysNext: true }, (element) => {
|
|
266
|
+
// The element's declaration: the source's own name (so aliasing checks find its uses) bound to the element.
|
|
267
|
+
const decl = f.createVariableDeclaration(d.name, undefined, undefined, element);
|
|
268
|
+
ts.setTextRange(decl, d);
|
|
269
|
+
decl.parent = list;
|
|
270
|
+
const declList = { ...list, flags: ts.NodeFlags.Const, declarations: [decl] };
|
|
271
|
+
this.variable(declList);
|
|
272
|
+
this.loops.push(label);
|
|
273
|
+
this.line(`{`);
|
|
274
|
+
this.block(() => this.body(stmt.statement));
|
|
275
|
+
this.line(`}`);
|
|
276
|
+
this.loops.pop();
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
/** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
|
|
280
|
+
isPipeline(e) {
|
|
281
|
+
return ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && (e.expression.name.text === "map" || e.expression.name.text === "filter");
|
|
282
|
+
}
|
|
283
|
+
/** The list under a chain of map / filter calls, and the calls in the order they run. */
|
|
284
|
+
pipelineStages(e) {
|
|
285
|
+
const stages = [];
|
|
286
|
+
let base = e;
|
|
287
|
+
while (this.isPipeline(base)) {
|
|
288
|
+
const access = base.expression;
|
|
289
|
+
const kind = access.name.text;
|
|
290
|
+
if (base.arguments.length !== 1)
|
|
291
|
+
this.fail(base, `${kind}() takes one argument: the callback`);
|
|
292
|
+
const fn = base.arguments[0];
|
|
293
|
+
if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
|
|
294
|
+
this.fail(fn, `pass ${kind} an arrow function written here: xs.${kind}((x) => …)`);
|
|
295
|
+
if (fn.parameters.length > 2)
|
|
296
|
+
this.fail(fn.parameters[2], `${kind}'s callback takes the element and its index`);
|
|
297
|
+
stages.unshift({ kind, fn });
|
|
298
|
+
base = access.expression;
|
|
299
|
+
}
|
|
300
|
+
if (stages.length) {
|
|
301
|
+
const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
|
|
302
|
+
if (!(l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(base)))
|
|
303
|
+
this.fail(base, "map and filter work on a Vec, FixedArray or FixedVec");
|
|
304
|
+
}
|
|
305
|
+
return { base, stages };
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Run `body` for each element of a list, or of a chain of `map` / `filter` calls on one (`xs.filter(f).map(g)`):
|
|
309
|
+
* one index loop over the list (its length read every step, as `for … of`), each callback expanded in it. A
|
|
310
|
+
* filter skips to the next element, a map binds its result, so a chain builds no list. `body` gets the element (as
|
|
311
|
+
* `list.get(i)` gives it, structs by reference, or the mapped value) and its index in the chain's output (after a
|
|
312
|
+
* filter, the count of elements it let through, as JS gives callbacks).
|
|
313
|
+
*/
|
|
314
|
+
eachElement(list, at, names, body) {
|
|
315
|
+
const f = ts.factory;
|
|
316
|
+
const place = (n) => {
|
|
317
|
+
ts.setTextRange(n, at);
|
|
318
|
+
n.parent = at;
|
|
319
|
+
return n;
|
|
320
|
+
};
|
|
321
|
+
const { base, stages } = this.pipelineStages(list);
|
|
322
|
+
const { index, ci } = names;
|
|
323
|
+
const indexId = place(f.createIdentifier(index));
|
|
324
|
+
const length = place(f.createPropertyAccessExpression(base, "length"));
|
|
325
|
+
const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(base, "get")), undefined, [indexId]));
|
|
326
|
+
const next = names.next ?? (stages.some((s) => s.kind === "filter") ? this.tmp("ts_next_") : undefined);
|
|
272
327
|
this.line(`{`);
|
|
273
328
|
this.block(() => {
|
|
274
329
|
this.line(`uint64_t ${ci} = 0;`);
|
|
275
330
|
this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
|
|
331
|
+
const counters = stages
|
|
332
|
+
.filter((st) => st.kind === "filter")
|
|
333
|
+
.map(() => {
|
|
334
|
+
const k = this.tmp("lm_k");
|
|
335
|
+
this.line(`uint64_t ${k} = 0;`);
|
|
336
|
+
return k;
|
|
337
|
+
});
|
|
276
338
|
this.line(`while (1) {`);
|
|
277
339
|
this.block(() => {
|
|
278
|
-
const n = this.expect(this.expr(length, U64), U64,
|
|
340
|
+
const n = this.expect(this.expr(length, U64), U64, base);
|
|
279
341
|
this.line(`if (!(${ci} < ${n.c})) break;`);
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
342
|
+
let element = get;
|
|
343
|
+
let idx = indexId;
|
|
344
|
+
let k = 0;
|
|
345
|
+
for (const st of stages) {
|
|
346
|
+
const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes] };
|
|
347
|
+
const v = this.expandClosure(closure, [element, idx].slice(0, st.fn.parameters.length), st.fn);
|
|
348
|
+
if (st.kind === "filter") {
|
|
349
|
+
if (!v)
|
|
350
|
+
this.fail(st.fn, "filter's callback returns a boolean");
|
|
351
|
+
const b = this.expect(v, BOOL, st.fn);
|
|
352
|
+
this.line(`if (!(${b.c})) goto ${next};`);
|
|
353
|
+
const id = `__lk${this.temp++}`;
|
|
354
|
+
const ic = this.tmp("lm_ki");
|
|
355
|
+
this.line(`uint64_t ${ic} = ${counters[k++]}++;`);
|
|
356
|
+
this.scopes.at(-1).set(id, { c: ic, type: U64, mutable: false });
|
|
357
|
+
idx = place(f.createIdentifier(id));
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
if (!v)
|
|
361
|
+
this.fail(st.fn, "map's callback returns the new element");
|
|
362
|
+
const mv = v.literal !== undefined ? this.literal(v.literal, U64, st.fn) : v;
|
|
363
|
+
if (!["int", "bool", "pubkey", "enum"].includes(mv.type.kind))
|
|
364
|
+
this.fail(st.fn, `map's callback returns integers, booleans, Pubkeys or enums (the new list's elements), not ${typeName(mv.type)}`);
|
|
365
|
+
const id = `__lv${this.temp++}`;
|
|
366
|
+
const mc = this.tmp("lm_v");
|
|
367
|
+
this.line(mv.type.kind === "pubkey" ? `SolPubkey ${mc} = *(${mv.c});` : `${valueC(mv.type)} ${mc} = ${mv.c};`);
|
|
368
|
+
this.scopes.at(-1).set(id, { c: mc, type: mv.type, mutable: false });
|
|
369
|
+
element = place(f.createIdentifier(id));
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
body(element, idx);
|
|
373
|
+
if (next && (names.alwaysNext || counters.length))
|
|
374
|
+
this.line(`${next}:;`);
|
|
287
375
|
this.line(`${ci}++;`);
|
|
288
376
|
});
|
|
289
377
|
this.line(`}`);
|
|
290
378
|
});
|
|
291
379
|
this.line(`}`);
|
|
292
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* `const ys = xs.map(…)` / `xs.filter(…)`: the chain's elements in a local FixedVec, as big as the list it starts
|
|
383
|
+
* from (a FixedArray's length, a FixedVec's capacity) or as annotated (`const ys: FixedVec<u64, 16> = …`, needed
|
|
384
|
+
* when it starts from a Vec). It lives on the stack, so it holds at most 2 KiB; one more element than it holds aborts.
|
|
385
|
+
*/
|
|
386
|
+
localList(d, init, c) {
|
|
387
|
+
const { base, stages } = this.pipelineStages(init);
|
|
388
|
+
let cap;
|
|
389
|
+
let annotatedElem;
|
|
390
|
+
if (d.type) {
|
|
391
|
+
const t = d.type;
|
|
392
|
+
const args = ts.isTypeReferenceNode(t) ? (t.typeArguments ?? []) : [];
|
|
393
|
+
const n = args[1] && ts.isLiteralTypeNode(args[1]) && ts.isNumericLiteral(args[1].literal) ? Number(args[1].literal.text) : NaN;
|
|
394
|
+
const elemNode = args[0];
|
|
395
|
+
const sn = elemNode && structNameOf(elemNode, this.scope);
|
|
396
|
+
annotatedElem = elemNode
|
|
397
|
+
? (resolveScalarType(elemNode, this.scope) ?? (sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : undefined))
|
|
398
|
+
: undefined;
|
|
399
|
+
if (!ts.isTypeReferenceNode(t) || typesolName(t, this.scope.imports) !== "FixedVec" || !annotatedElem || !Number.isInteger(n) || n < 1)
|
|
400
|
+
this.fail(t, "annotate the new list as FixedVec<T, N> (N a literal)");
|
|
401
|
+
cap = n;
|
|
402
|
+
}
|
|
403
|
+
else {
|
|
404
|
+
const r = this.reporter.guard(() => this.ref(base));
|
|
405
|
+
const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
|
|
406
|
+
if (r?.type.kind === "array")
|
|
407
|
+
cap = r.type.len;
|
|
408
|
+
else if (r?.type.kind === "vec" && r.type.cap !== undefined)
|
|
409
|
+
cap = r.type.cap;
|
|
410
|
+
if (cap === undefined || l?.type.kind === "vecArg")
|
|
411
|
+
this.fail(init, "this list has no fixed size: annotate the new one with its capacity, const ys: FixedVec<T, N> = …");
|
|
412
|
+
}
|
|
413
|
+
// The element type: the last map's result, else the list's own elements (known once the loop is emitted).
|
|
414
|
+
const declAt = this.lines.length;
|
|
415
|
+
this.line(`/* ${c} */`);
|
|
416
|
+
let elem = stages.some((st) => st.kind === "map") ? undefined : this.listElem(base);
|
|
417
|
+
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
|
+
});
|
|
433
|
+
const t = { kind: "vec", elem: annotatedElem ?? elem, cap: cap0 };
|
|
434
|
+
const bytes = 4 + this.fieldBytes(t.elem) * cap0;
|
|
435
|
+
if (bytes > 2048)
|
|
436
|
+
this.fail(init, `the new list takes ${bytes} bytes; locals live on the 4 KiB stack, so keep it under 2 KiB (a smaller FixedVec<T, N>)`);
|
|
437
|
+
this.model.localVecs.set(fixedVecC(t), t);
|
|
438
|
+
const pad = " ".repeat(this.indent);
|
|
439
|
+
this.lines[declAt] = `${pad}${fixedVecC(t)} ${c};\n${pad}ts_memzero(&${c}, sizeof(${c}));`;
|
|
440
|
+
return t;
|
|
441
|
+
}
|
|
442
|
+
/** The element type of a Vec, FixedArray, FixedVec or Vec argument. */
|
|
443
|
+
listElem(list) {
|
|
444
|
+
const l = ts.isIdentifier(list) ? this.lookup(list.text) : undefined;
|
|
445
|
+
if (l?.type.kind === "vecArg")
|
|
446
|
+
return l.type.elem;
|
|
447
|
+
const r = this.ref(list);
|
|
448
|
+
if (r && (r.type.kind === "array" || r.type.kind === "vec"))
|
|
449
|
+
return r.type.elem;
|
|
450
|
+
this.fail(list, "expected a Vec, FixedArray or FixedVec");
|
|
451
|
+
}
|
|
293
452
|
/** Whether `e` is the remaining accounts (or a const bound to them). */
|
|
294
453
|
isRemaining(e) {
|
|
295
454
|
if (ts.isCallExpression(e) && this.exportName(e.expression) === "remainingAccounts")
|
|
@@ -337,6 +496,11 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
337
496
|
this.fail(p, "closure parameters are plain names");
|
|
338
497
|
const arg = args[i];
|
|
339
498
|
const name = p.name.text;
|
|
499
|
+
// A function parameter (of a helper expanded here): the function given, expanded where the body calls it.
|
|
500
|
+
if (p.type && ts.isFunctionTypeNode(p.type)) {
|
|
501
|
+
params.set(name, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
340
504
|
const c2 = `l_${name}_${this.temp++}`;
|
|
341
505
|
const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
|
|
342
506
|
if (!annotated && this.looksLikeStructPlace(arg)) {
|
|
@@ -378,6 +542,8 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
378
542
|
this.line(`{`);
|
|
379
543
|
this.block(() => {
|
|
380
544
|
const body = fn.body;
|
|
545
|
+
if (!body)
|
|
546
|
+
this.fail(fn, `${c.name} has no body`);
|
|
381
547
|
if (ts.isBlock(body))
|
|
382
548
|
body.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
|
|
383
549
|
else {
|
|
@@ -434,9 +600,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
434
600
|
this.fail(cb, `pass ${method} an arrow function written here: xs.${method}((x) => …)`);
|
|
435
601
|
const fn = cb;
|
|
436
602
|
const index = `__lm${this.temp++}`;
|
|
437
|
-
|
|
438
|
-
const length = place(f.createPropertyAccessExpression(list, "length"));
|
|
439
|
-
const element = place(f.createCallExpression(place(f.createPropertyAccessExpression(list, "get")), undefined, [indexId]));
|
|
603
|
+
void place;
|
|
440
604
|
const I64 = { kind: "int", name: "i64" };
|
|
441
605
|
// The result, declared before the loop.
|
|
442
606
|
const res = this.tmp("lm");
|
|
@@ -482,69 +646,58 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
482
646
|
}
|
|
483
647
|
const findDecl = method === "find" ? this.lines.length - 1 : -1;
|
|
484
648
|
const ci = this.tmp("lm_i");
|
|
485
|
-
this.
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
this.line(`
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
this.
|
|
499
|
-
|
|
649
|
+
this.eachElement(list, node, { index, ci }, (element, indexId) => {
|
|
650
|
+
if (valueForm) {
|
|
651
|
+
const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
|
|
652
|
+
const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
|
|
653
|
+
this.line(`if (${hit.c}) {`);
|
|
654
|
+
this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${this.expr(indexId, U64).c};`);
|
|
655
|
+
this.line(` break;`);
|
|
656
|
+
this.line(`}`);
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
|
|
660
|
+
const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
|
|
661
|
+
if (accId)
|
|
662
|
+
this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
|
|
663
|
+
const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
|
|
664
|
+
const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
|
|
665
|
+
if (method === "forEach") {
|
|
666
|
+
if (v)
|
|
667
|
+
this.fail(cb, "forEach's callback returns nothing");
|
|
668
|
+
}
|
|
669
|
+
else if (method === "reduce") {
|
|
670
|
+
if (!v)
|
|
671
|
+
this.fail(cb, "reduce's callback returns the new accumulator");
|
|
672
|
+
const nv = this.expect(v, accType, cb);
|
|
673
|
+
this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
|
|
500
674
|
}
|
|
501
675
|
else {
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
this.
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
this.
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
this.line(`if (${b.c}) { ${res} = true; break; }`);
|
|
524
|
-
if (method === "every")
|
|
525
|
-
this.line(`if (!(${b.c})) { ${res} = false; break; }`);
|
|
526
|
-
if (method === "findIndex")
|
|
527
|
-
this.line(`if (${b.c}) { ${res} = (int64_t)${ci}; break; }`);
|
|
528
|
-
if (method === "find") {
|
|
529
|
-
this.line(`if (${b.c}) {`);
|
|
530
|
-
this.block(() => {
|
|
531
|
-
const e = this.expr(element);
|
|
532
|
-
elemType = e.type;
|
|
533
|
-
if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
|
|
534
|
-
this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
|
|
535
|
-
this.line(`${res}.some = 1;`);
|
|
536
|
-
this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
|
|
537
|
-
this.line(`break;`);
|
|
538
|
-
});
|
|
539
|
-
this.line(`}`);
|
|
540
|
-
}
|
|
676
|
+
if (!v)
|
|
677
|
+
this.fail(cb, `${method}'s callback returns a boolean`);
|
|
678
|
+
const b = this.expect(v, BOOL, cb);
|
|
679
|
+
if (method === "some")
|
|
680
|
+
this.line(`if (${b.c}) { ${res} = true; break; }`);
|
|
681
|
+
if (method === "every")
|
|
682
|
+
this.line(`if (!(${b.c})) { ${res} = false; break; }`);
|
|
683
|
+
if (method === "findIndex")
|
|
684
|
+
this.line(`if (${b.c}) { ${res} = (int64_t)${this.expr(indexId, U64).c}; break; }`);
|
|
685
|
+
if (method === "find") {
|
|
686
|
+
this.line(`if (${b.c}) {`);
|
|
687
|
+
this.block(() => {
|
|
688
|
+
const e = this.expr(element);
|
|
689
|
+
elemType = e.type;
|
|
690
|
+
if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
|
|
691
|
+
this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
|
|
692
|
+
this.line(`${res}.some = 1;`);
|
|
693
|
+
this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
|
|
694
|
+
this.line(`break;`);
|
|
695
|
+
});
|
|
696
|
+
this.line(`}`);
|
|
541
697
|
}
|
|
542
698
|
}
|
|
543
|
-
|
|
544
|
-
});
|
|
545
|
-
this.line(`}`);
|
|
699
|
+
}
|
|
546
700
|
});
|
|
547
|
-
this.line(`}`);
|
|
548
701
|
if (method === "find") {
|
|
549
702
|
const inner = elemType;
|
|
550
703
|
this.model.options.set(optionKey(inner), inner);
|
|
@@ -650,6 +803,11 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
650
803
|
this.scopes.at(-1).set(name, { c, type: t, mutable });
|
|
651
804
|
continue;
|
|
652
805
|
}
|
|
806
|
+
if (this.isPipeline(init)) {
|
|
807
|
+
const t = this.localList(d, init, c);
|
|
808
|
+
this.scopes.at(-1).set(name, { c, type: t, mutable });
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
653
811
|
if (fn === "loadAccount" || fn === "loadAccountMut" || fn === "loadForeign") {
|
|
654
812
|
if (mutable)
|
|
655
813
|
this.fail(d, "bind loaded accounts with const");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
2
|
import { resolveScalarType } from "../analyze.js";
|
|
3
3
|
import { INT_INFO, isScalar, isWritable, sameType, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount } from "./shared.js";
|
|
4
|
+
import { U64, BOOL, PUBKEY, STRING, optC, fieldC, fixedVecC, storageDecl, vecCount, vecPlace, emptySizeC } from "./shared.js";
|
|
5
5
|
import { EmitterBase } from "./base.js";
|
|
6
6
|
/**
|
|
7
7
|
* Places in account data and locals: references, reads and writes, Vec/array indexing, aliasing rules and
|
|
@@ -258,6 +258,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
258
258
|
const l = this.lookup(node.text);
|
|
259
259
|
if (l && l.type.kind === "array")
|
|
260
260
|
return { lv: l.c, type: l.type, writable: !l.readonly };
|
|
261
|
+
if (l && l.type.kind === "vec")
|
|
262
|
+
return this.withFixedVec({ lv: l.c, type: l.type, writable: true });
|
|
261
263
|
// A struct loaded from a remaining account.
|
|
262
264
|
if (l && l.type.kind === "structRef")
|
|
263
265
|
return { lv: `(*${l.c})`, type: { kind: "struct", name: l.type.struct }, writable: l.type.writable };
|
|
@@ -278,15 +280,35 @@ export class StorageEmitter extends EmitterBase {
|
|
|
278
280
|
const r = { lv: `d_${a.name}->${f.name}`, type: f.type, writable: isWritable(a.kind), account: a, path: [f.name] };
|
|
279
281
|
if (f.type.kind === "vec" && f.type.cap !== undefined)
|
|
280
282
|
r.vec = this.fixedVec(r.lv, f.type.elem, f.type.cap);
|
|
281
|
-
else if (f.type.kind === "vec")
|
|
283
|
+
else if (f.type.kind === "vec") {
|
|
282
284
|
// Growing needs the account's length at instruction entry: instructions have it, helpers get it passed.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
285
|
+
const s = a.kind.struct;
|
|
286
|
+
const k = s.vecs.findIndex((v) => v.name === f.name);
|
|
287
|
+
if (s.vecs.length === 1)
|
|
288
|
+
r.vec = {
|
|
289
|
+
len: `d_${a.name}->${f.name}__len`,
|
|
290
|
+
items: r.lv,
|
|
291
|
+
fixed: `sizeof(TSS_${s.name})`,
|
|
292
|
+
elemSize: s.vec.elemSize,
|
|
293
|
+
resizable: isWritable(a.kind),
|
|
294
|
+
};
|
|
295
|
+
else {
|
|
296
|
+
const base = `(*d_${a.name})`;
|
|
297
|
+
const at = vecPlace(s, base, k);
|
|
298
|
+
const others = s.vecs.map((v, j) => (j === k ? "" : ` + (uint64_t)${vecPlace(s, base, j).len} * ${v.elemSize}`)).join("");
|
|
299
|
+
r.lv = at.items;
|
|
300
|
+
r.vec = {
|
|
301
|
+
len: at.len,
|
|
302
|
+
items: at.items,
|
|
303
|
+
fixed: `(${emptySizeC(s)}${others})`,
|
|
304
|
+
elemSize: s.vecs[k].elemSize,
|
|
305
|
+
resizable: isWritable(a.kind),
|
|
306
|
+
tail: k < s.vecs.length - 1
|
|
307
|
+
? { end: at.end, dataEnd: `(a_${a.name}->data + a_${a.name}->data_len)`, paths: s.vecs.slice(k + 1).map((v) => `${a.name}.${v.name}[]`) }
|
|
308
|
+
: undefined,
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
290
312
|
return r;
|
|
291
313
|
}
|
|
292
314
|
}
|
|
@@ -330,8 +352,12 @@ export class StorageEmitter extends EmitterBase {
|
|
|
330
352
|
this.fail(node.name, `${s.name} has no field "${field}"`);
|
|
331
353
|
const r = { ...base, lv: `${base.lv}.${f.name}`, type: f.type, path: base.path && [...base.path, f.name], vec: undefined };
|
|
332
354
|
// Vecs of loaded accounts can be read and updated in place but not resized (no entry length known).
|
|
333
|
-
if (f.type.kind === "vec" && f.type.cap === undefined)
|
|
334
|
-
|
|
355
|
+
if (f.type.kind === "vec" && f.type.cap === undefined) {
|
|
356
|
+
const k = s.vecs.findIndex((v) => v.name === f.name);
|
|
357
|
+
const at = s.vecs.length === 1 ? { len: `${base.lv}.${f.name}__len`, items: r.lv } : vecPlace(s, base.lv, k);
|
|
358
|
+
r.lv = at.items;
|
|
359
|
+
r.vec = { len: at.len, items: at.items, fixed: emptySizeC(s), elemSize: s.vecs[k].elemSize, resizable: false };
|
|
360
|
+
}
|
|
335
361
|
return this.withFixedVec(r);
|
|
336
362
|
}
|
|
337
363
|
return undefined;
|
|
@@ -671,8 +697,8 @@ export class StorageEmitter extends EmitterBase {
|
|
|
671
697
|
const l = this.lookup(node.text);
|
|
672
698
|
if (!l)
|
|
673
699
|
this.fail(node, `"${node.text}" is not a local variable`);
|
|
674
|
-
if (l.type.kind === "array")
|
|
675
|
-
this.fail(node, `use ${node.text}.set(i, v) to change
|
|
700
|
+
if (l.type.kind === "array" || l.type.kind === "vec")
|
|
701
|
+
this.fail(node, `use ${node.text}.set(i, v) to change list elements`);
|
|
676
702
|
if (!l.mutable)
|
|
677
703
|
this.fail(node, `"${node.text}" is const or an argument and cannot be reassigned`);
|
|
678
704
|
if (l.type.kind === "pubkey")
|
package/dist/compiler/model.d.ts
CHANGED
|
@@ -38,9 +38,11 @@ export type ValueType = ScalarType | {
|
|
|
38
38
|
inner: ScalarType;
|
|
39
39
|
}
|
|
40
40
|
/** `Vec<T>` instruction argument: a read-only view into instruction data. T is a scalar or a fixed-size struct. */
|
|
41
|
+
/** A Vec argument (Borsh), read in place; `FixedVec<T, N>` arguments are the same with at most `cap` elements. */
|
|
41
42
|
| {
|
|
42
43
|
kind: "vecArg";
|
|
43
44
|
elem: FieldType;
|
|
45
|
+
cap?: number;
|
|
44
46
|
}
|
|
45
47
|
/** `FixedArray<T, N>` instruction argument (Borsh [T; N]), parsed into a read-only copy. */
|
|
46
48
|
| {
|
|
@@ -119,6 +121,8 @@ export type FieldType = ScalarType | {
|
|
|
119
121
|
export interface StructField {
|
|
120
122
|
name: string;
|
|
121
123
|
type: FieldType;
|
|
124
|
+
/** The declaration, while the struct is being analyzed (for messages). */
|
|
125
|
+
node?: ts.Node;
|
|
122
126
|
}
|
|
123
127
|
/** Every exported interface. Used as account data, nested in other structs, or emitted as an event. */
|
|
124
128
|
export interface StructDef {
|
|
@@ -129,12 +133,21 @@ export interface StructDef {
|
|
|
129
133
|
* or undefined when a field is a variable-length string (event-only structs).
|
|
130
134
|
*/
|
|
131
135
|
size: number | undefined;
|
|
132
|
-
/** The
|
|
136
|
+
/** The first growable Vec field, if any. Such structs can only be account data. */
|
|
133
137
|
vec?: {
|
|
134
138
|
name: string;
|
|
135
139
|
elem: FieldType;
|
|
136
140
|
elemSize: number;
|
|
137
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* Every growable Vec, in stored order (after the fixed-size fields). The first ends the C struct (its u32 length,
|
|
144
|
+
* then its elements); each further one follows the previous one's elements: a u32 length, then its elements.
|
|
145
|
+
*/
|
|
146
|
+
vecs?: {
|
|
147
|
+
name: string;
|
|
148
|
+
elem: FieldType;
|
|
149
|
+
elemSize: number;
|
|
150
|
+
}[];
|
|
138
151
|
accountDiscriminator: number[];
|
|
139
152
|
eventDiscriminator: number[];
|
|
140
153
|
node: ts.InterfaceDeclaration;
|
|
@@ -436,6 +449,10 @@ export interface ProgramModel {
|
|
|
436
449
|
fixedVecs: Map<string, FieldType & {
|
|
437
450
|
kind: "vec";
|
|
438
451
|
}>;
|
|
452
|
+
/** FixedVec types of local lists (`const ys = xs.map(…)`), filled in while emitting: C typedefs only, not the IDL. */
|
|
453
|
+
localVecs: Map<string, FieldType & {
|
|
454
|
+
kind: "vec";
|
|
455
|
+
}>;
|
|
439
456
|
}
|
|
440
457
|
export declare function isWritable(k: ParamKind): boolean;
|
|
441
458
|
export declare function typeName(t: ValueType | FieldType): string;
|