@typesol/cli 0.16.0 → 0.18.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 +121 -0
- package/README.md +4 -4
- package/dist/clientgen.js +39 -0
- package/dist/compiler/analyze.d.ts +16 -1
- package/dist/compiler/analyze.js +355 -43
- package/dist/compiler/emit/base.d.ts +36 -1
- package/dist/compiler/emit/base.js +135 -4
- package/dist/compiler/emit/borsh.d.ts +13 -0
- package/dist/compiler/emit/borsh.js +298 -0
- package/dist/compiler/emit/calls.d.ts +2 -3
- package/dist/compiler/emit/calls.js +135 -29
- package/dist/compiler/emit/cpi.d.ts +62 -8
- package/dist/compiler/emit/cpi.js +1458 -40
- package/dist/compiler/emit/expressions.d.ts +5 -0
- package/dist/compiler/emit/expressions.js +85 -5
- package/dist/compiler/emit/function.js +82 -14
- package/dist/compiler/emit/program.js +24 -6
- package/dist/compiler/emit/shared.d.ts +20 -0
- package/dist/compiler/emit/shared.js +23 -3
- package/dist/compiler/emit/statements.d.ts +21 -0
- package/dist/compiler/emit/statements.js +380 -19
- package/dist/compiler/emit/storage.d.ts +6 -0
- package/dist/compiler/emit/storage.js +88 -16
- package/dist/compiler/idl.js +37 -5
- package/dist/compiler/model.d.ts +70 -4
- package/dist/compiler/model.js +8 -2
- package/dist/portgen.js +90 -4
- package/docs/errors.md +6 -4
- package/docs/language.md +32 -20
- package/docs/migrate-from-anchor.md +24 -7
- package/docs/test-your-program.md +1 -1
- package/lib/typesol.d.ts +652 -21
- package/package.json +1 -1
- package/runtime/typesol_rt.h +230 -7
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import ts from "typescript";
|
|
2
|
-
import { resolveValueType } from "../analyze.js";
|
|
3
|
-
import { INT_INFO, isWritable, scalarSize, typeName } from "../model.js";
|
|
4
|
-
import { U64, BOOL, cString, scalarC, scalarStorageC, valueC } from "./shared.js";
|
|
2
|
+
import { MAX_RETURN_DATA, resolveValueType, structNameOf } from "../analyze.js";
|
|
3
|
+
import { INT_INFO, isWritable, optionKey, scalarSize, typeName } from "../model.js";
|
|
4
|
+
import { U64, BOOL, cString, 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.
|
|
@@ -65,11 +65,16 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
65
65
|
this.line(ts.isBreakStatement(stmt) ? `break;` : `goto ${this.loops.at(-1)};`);
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
-
if (ts.isDoStatement(stmt)
|
|
69
|
-
this.
|
|
68
|
+
if (ts.isDoStatement(stmt))
|
|
69
|
+
return this.doLoop(stmt);
|
|
70
|
+
if (ts.isForInStatement(stmt))
|
|
71
|
+
this.fail(stmt, "for … in is not supported: use for (init; cond; step), for (const x of list), while or do … while");
|
|
70
72
|
this.fail(stmt, `${ts.SyntaxKind[stmt.kind]} is not supported in TypeSOL`);
|
|
71
73
|
}
|
|
72
74
|
returnStatement(stmt) {
|
|
75
|
+
const frame = this.inlineFrames.at(-1);
|
|
76
|
+
if (frame)
|
|
77
|
+
return this.closureReturn(stmt, frame);
|
|
73
78
|
const returns = this.helper ? this.helper.returns : this.ix.returns;
|
|
74
79
|
if (!returns) {
|
|
75
80
|
if (stmt.expression)
|
|
@@ -92,7 +97,18 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
92
97
|
const t = this.tmp("ret");
|
|
93
98
|
this.line(`TSS_${returns.name} ${t};`);
|
|
94
99
|
this.writeAggregate({ lv: t, type: returns, writable: true }, stmt.expression);
|
|
95
|
-
this.
|
|
100
|
+
if (this.model.structs.get(returns.name).layout === "borsh") {
|
|
101
|
+
// A @layout borsh struct returns its Borsh bytes, as Anchor's return data does.
|
|
102
|
+
const b = this.tmp("retb");
|
|
103
|
+
const w = this.tmp("retw");
|
|
104
|
+
this.line(`uint8_t *${b};`);
|
|
105
|
+
this.line(`TS_TRY(ts_alloc(sizeof(${t}), &${b}));`);
|
|
106
|
+
this.line(`uint64_t ${w} = 0;`);
|
|
107
|
+
this.line(`TS_TRY(ts_pack_${returns.name}(&${t}, ${b}, sizeof(${t}), &${w}));`);
|
|
108
|
+
this.line(`sol_set_return_data(${b}, ${w});`);
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
this.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
|
|
96
112
|
for (const l of this.epilogue())
|
|
97
113
|
this.line(l);
|
|
98
114
|
this.line(`return SUCCESS;`);
|
|
@@ -101,8 +117,24 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
101
117
|
if (!stmt.expression)
|
|
102
118
|
this.fail(stmt, `return a ${typeName(returns)}`);
|
|
103
119
|
const v = this.expect(this.expr(stmt.expression, returns), returns, stmt.expression);
|
|
104
|
-
if (this.helper)
|
|
120
|
+
if (this.helper && returns.kind === "string") {
|
|
121
|
+
// The caller gets its own copy: the text may be account data the caller then changes.
|
|
122
|
+
this.line(`TS_TRY(ts_str_copy(${v.c}, ret));`);
|
|
123
|
+
}
|
|
124
|
+
else if (this.helper)
|
|
105
125
|
this.line(returns.kind === "pubkey" ? `*ret = *(${v.c});` : `*ret = ${v.c};`);
|
|
126
|
+
else if (returns.kind === "string") {
|
|
127
|
+
// Return data is the Borsh String: u32 length, then the bytes (at most 1024 in all).
|
|
128
|
+
const s = this.tmp("rets");
|
|
129
|
+
const b = this.tmp("retb");
|
|
130
|
+
this.line(`TS_Str ${s} = ${v.c};`);
|
|
131
|
+
this.line(`if (${s}.len > ${MAX_RETURN_DATA - 4}) TS_FAIL(TS_ERR_INSTRUCTION_DATA, "TypeSOL: returned string longer than ${MAX_RETURN_DATA - 4} bytes");`);
|
|
132
|
+
this.line(`uint8_t *${b};`);
|
|
133
|
+
this.line(`TS_TRY(ts_alloc(4 + ${s}.len, &${b}));`);
|
|
134
|
+
this.line(`ts_write_le(${b}, ${s}.len, 4);`);
|
|
135
|
+
this.line(`ts_memcpy(${b} + 4, ${s}.ptr, ${s}.len);`);
|
|
136
|
+
this.line(`sol_set_return_data(${b}, 4 + ${s}.len);`);
|
|
137
|
+
}
|
|
106
138
|
else if (returns.kind === "pubkey")
|
|
107
139
|
this.line(`sol_set_return_data((const uint8_t *)(${v.c}), 32);`);
|
|
108
140
|
else {
|
|
@@ -146,11 +178,16 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
146
178
|
const typed = subject.literal !== undefined ? this.literal(subject.literal, U64, stmt.expression) : subject;
|
|
147
179
|
if (typed.type.kind !== "int" && typed.type.kind !== "enum")
|
|
148
180
|
this.fail(stmt.expression, "switch works on integers and enums");
|
|
149
|
-
|
|
150
|
-
|
|
181
|
+
// C cannot switch on 128-bit values: pick the case's index with comparisons, then switch on that.
|
|
182
|
+
const wide = typed.type.kind === "int" && INT_INFO[typed.type.name].bytes === 16;
|
|
151
183
|
const t = this.tmp("sw");
|
|
152
184
|
this.line(`${scalarC(typed.type)} ${t} = ${typed.c};`);
|
|
153
|
-
this.
|
|
185
|
+
const pick = wide ? this.tmp("swi") : undefined;
|
|
186
|
+
const pickAt = this.lines.length;
|
|
187
|
+
const cases = [];
|
|
188
|
+
if (pick)
|
|
189
|
+
this.line(`int ${pick} = -1;`);
|
|
190
|
+
this.line(`switch (${pick ?? t}) {`);
|
|
154
191
|
const seen = new Set();
|
|
155
192
|
for (const clause of stmt.caseBlock.clauses) {
|
|
156
193
|
if (ts.isCaseClause(clause)) {
|
|
@@ -166,7 +203,12 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
166
203
|
if (seen.has(label.c))
|
|
167
204
|
this.fail(clause.expression, "duplicate case");
|
|
168
205
|
seen.add(label.c);
|
|
169
|
-
|
|
206
|
+
if (pick) {
|
|
207
|
+
cases.push(label.c);
|
|
208
|
+
this.line(`case ${cases.length - 1}: {`);
|
|
209
|
+
}
|
|
210
|
+
else
|
|
211
|
+
this.line(`case ${label.c}: {`);
|
|
170
212
|
}
|
|
171
213
|
else
|
|
172
214
|
this.line(`default: {`);
|
|
@@ -176,6 +218,10 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
176
218
|
this.line(`}`);
|
|
177
219
|
}
|
|
178
220
|
this.line(`}`);
|
|
221
|
+
if (pick && cases.length) {
|
|
222
|
+
const pad = " ".repeat(this.indent);
|
|
223
|
+
this.lines.splice(pickAt + 1, 0, ...cases.map((c, i) => `${pad}${i ? "else " : ""}if (${t} == ${c}) ${pick} = ${i};`));
|
|
224
|
+
}
|
|
179
225
|
}
|
|
180
226
|
forLoop(stmt) {
|
|
181
227
|
this.line(`{`);
|
|
@@ -273,6 +319,290 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
273
319
|
});
|
|
274
320
|
this.line(`}`);
|
|
275
321
|
}
|
|
322
|
+
/**
|
|
323
|
+
* Expand a closure where it is called: the arguments are evaluated here, in order, into the parameters (scalars,
|
|
324
|
+
* Pubkeys and strings by value, structs by reference, as JS passes objects); the body then runs with the names it
|
|
325
|
+
* was declared with, so a block around the call that declares the same name does not change what it reads, and
|
|
326
|
+
* writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
|
|
327
|
+
*/
|
|
328
|
+
expandClosure(c, args, node) {
|
|
329
|
+
const fn = c.node;
|
|
330
|
+
if (this.inlining.has(fn))
|
|
331
|
+
this.fail(node, `${c.name} calls itself: closures cannot be recursive`);
|
|
332
|
+
if (args.length !== fn.parameters.length)
|
|
333
|
+
this.fail(node, `${c.name}() takes ${fn.parameters.length} argument(s)`);
|
|
334
|
+
const params = new Map();
|
|
335
|
+
fn.parameters.forEach((p, i) => {
|
|
336
|
+
if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
|
|
337
|
+
this.fail(p, "closure parameters are plain names");
|
|
338
|
+
const arg = args[i];
|
|
339
|
+
const name = p.name.text;
|
|
340
|
+
const c2 = `l_${name}_${this.temp++}`;
|
|
341
|
+
const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
|
|
342
|
+
if (!annotated && this.looksLikeStructPlace(arg)) {
|
|
343
|
+
const place = this.structPlace(arg);
|
|
344
|
+
this.line(`TSS_${place.struct} *${c2} = ${place.ptr};`);
|
|
345
|
+
params.set(name, { c: c2, type: { kind: "structRef", struct: place.struct, writable: place.writable }, mutable: false, paths: this.pathsOf(arg) });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
let v = this.expr(arg, annotated);
|
|
349
|
+
if (v.isNull && !annotated)
|
|
350
|
+
this.fail(arg, "give the parameter a type to pass null");
|
|
351
|
+
if (v.literal !== undefined && !annotated)
|
|
352
|
+
v = this.literal(v.literal, U64, arg);
|
|
353
|
+
const type = annotated ?? v.type;
|
|
354
|
+
v = this.expect(v, type, arg);
|
|
355
|
+
if (type.kind === "pubkey")
|
|
356
|
+
this.line(`SolPubkey ${c2} = *(${v.c});`);
|
|
357
|
+
else
|
|
358
|
+
this.line(`${valueC(type)}${type.kind === "account" ? "" : " "}${c2} = ${this.ownedString(v).c};`);
|
|
359
|
+
params.set(name, { c: c2, type, mutable: true });
|
|
360
|
+
});
|
|
361
|
+
const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, this.scope) : undefined;
|
|
362
|
+
if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword && !declared)
|
|
363
|
+
this.fail(fn.type, "closures return integers, booleans, Pubkeys, strings, enums or Options");
|
|
364
|
+
const frame = {
|
|
365
|
+
label: this.tmp("ts_ret_"),
|
|
366
|
+
result: this.tmp("ret"),
|
|
367
|
+
type: declared,
|
|
368
|
+
declAt: this.lines.length,
|
|
369
|
+
pad: " ".repeat(this.indent),
|
|
370
|
+
node: fn,
|
|
371
|
+
};
|
|
372
|
+
this.line(declared ? `${declared.kind === "pubkey" ? "SolPubkey" : valueC(declared)} ${frame.result};` : `/* ${frame.result} */`);
|
|
373
|
+
const saved = { scopes: this.scopes, loops: this.loops.splice(0) };
|
|
374
|
+
this.scopes = [...c.scopes, params];
|
|
375
|
+
this.inlining.add(fn);
|
|
376
|
+
this.inlineFrames.push(frame);
|
|
377
|
+
try {
|
|
378
|
+
this.line(`{`);
|
|
379
|
+
this.block(() => {
|
|
380
|
+
const body = fn.body;
|
|
381
|
+
if (ts.isBlock(body))
|
|
382
|
+
body.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
|
|
383
|
+
else {
|
|
384
|
+
// An expression body: its value is the result (a call may have none).
|
|
385
|
+
const ret = ts.factory.createReturnStatement(body);
|
|
386
|
+
ts.setTextRange(ret, body);
|
|
387
|
+
ret.parent = fn;
|
|
388
|
+
if (ts.isCallExpression(body) && !frame.type) {
|
|
389
|
+
const v = this.call(body, false);
|
|
390
|
+
if (v)
|
|
391
|
+
this.closureResult(v, body, frame);
|
|
392
|
+
}
|
|
393
|
+
else
|
|
394
|
+
this.closureReturn(ret, frame);
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
this.line(`}`);
|
|
398
|
+
this.line(`${frame.label}:;`);
|
|
399
|
+
}
|
|
400
|
+
finally {
|
|
401
|
+
this.inlineFrames.pop();
|
|
402
|
+
this.inlining.delete(fn);
|
|
403
|
+
this.scopes = saved.scopes;
|
|
404
|
+
this.loops.push(...saved.loops);
|
|
405
|
+
}
|
|
406
|
+
if (!frame.type) {
|
|
407
|
+
if (this.lines[frame.declAt].includes(`/* ${frame.result} */`))
|
|
408
|
+
this.lines[frame.declAt] = "";
|
|
409
|
+
return undefined;
|
|
410
|
+
}
|
|
411
|
+
return frame.type.kind === "pubkey" ? { c: `(&${frame.result})`, type: frame.type } : { c: frame.result, type: frame.type };
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* `xs.forEach / some / every / find / findIndex / reduce / includes / indexOf` on a Vec, FixedArray, FixedVec or Vec
|
|
415
|
+
* argument: an index loop (the length read each step, as `for … of`), the callback (an arrow written at the call)
|
|
416
|
+
* expanded with each element as `xs.get(i)` gives it (structs by reference) and, if it takes one, the index.
|
|
417
|
+
* `find` gives an Option of the element (scalar elements), `findIndex` / `indexOf` an i64 (-1 when absent).
|
|
418
|
+
*/
|
|
419
|
+
listMethod(node, method) {
|
|
420
|
+
const access = node.expression;
|
|
421
|
+
const list = access.expression;
|
|
422
|
+
const args = node.arguments;
|
|
423
|
+
const f = ts.factory;
|
|
424
|
+
const place = (n) => {
|
|
425
|
+
ts.setTextRange(n, node);
|
|
426
|
+
n.parent = node;
|
|
427
|
+
return n;
|
|
428
|
+
};
|
|
429
|
+
const valueForm = method === "includes" || method === "indexOf";
|
|
430
|
+
if (args.length !== (method === "reduce" ? 2 : 1))
|
|
431
|
+
this.fail(node, method === "reduce" ? "reduce(callback, initial) takes two arguments" : `${method}() takes one argument`);
|
|
432
|
+
const cb = args[0];
|
|
433
|
+
if (!valueForm && !ts.isArrowFunction(cb) && !ts.isFunctionExpression(cb))
|
|
434
|
+
this.fail(cb, `pass ${method} an arrow function written here: xs.${method}((x) => …)`);
|
|
435
|
+
const fn = cb;
|
|
436
|
+
const index = `__lm${this.temp++}`;
|
|
437
|
+
const indexId = place(f.createIdentifier(index));
|
|
438
|
+
const length = place(f.createPropertyAccessExpression(list, "length"));
|
|
439
|
+
const element = place(f.createCallExpression(place(f.createPropertyAccessExpression(list, "get")), undefined, [indexId]));
|
|
440
|
+
const I64 = { kind: "int", name: "i64" };
|
|
441
|
+
// The result, declared before the loop.
|
|
442
|
+
const res = this.tmp("lm");
|
|
443
|
+
let result;
|
|
444
|
+
let elemType;
|
|
445
|
+
let accType;
|
|
446
|
+
let needle;
|
|
447
|
+
if (method === "some" || method === "includes")
|
|
448
|
+
(this.line(`bool ${res} = false;`), (result = { c: res, type: BOOL }));
|
|
449
|
+
if (method === "every")
|
|
450
|
+
(this.line(`bool ${res} = true;`), (result = { c: res, type: BOOL }));
|
|
451
|
+
if (method === "findIndex" || method === "indexOf")
|
|
452
|
+
(this.line(`int64_t ${res} = -1;`), (result = { c: res, type: I64 }));
|
|
453
|
+
if (method === "reduce") {
|
|
454
|
+
const accParam = fn.parameters[0];
|
|
455
|
+
const annotated = accParam?.type ? resolveValueType(accParam.type, this.scope) : undefined;
|
|
456
|
+
let init = this.expr(args[1], annotated);
|
|
457
|
+
if (init.literal !== undefined && !annotated)
|
|
458
|
+
init = this.literal(init.literal, U64, args[1]);
|
|
459
|
+
accType = annotated ?? init.type;
|
|
460
|
+
init = this.expect(init, accType, args[1]);
|
|
461
|
+
if (!["int", "bool", "pubkey", "string", "enum", "option"].includes(accType.kind))
|
|
462
|
+
this.fail(args[1], `reduce accumulates integers, booleans, Pubkeys, strings, enums or Options, not ${typeName(accType)}`);
|
|
463
|
+
this.line(accType.kind === "pubkey" ? `SolPubkey ${res} = *(${init.c});` : `${valueC(accType)} ${res} = ${this.ownedString(init).c};`);
|
|
464
|
+
result = accType.kind === "pubkey" ? { c: `(&${res})`, type: accType } : { c: res, type: accType };
|
|
465
|
+
}
|
|
466
|
+
if (valueForm) {
|
|
467
|
+
// The value looked for, read once, compared with === (scalars and strings).
|
|
468
|
+
const v = this.expr(cb);
|
|
469
|
+
needle = `__lv${this.temp++}`;
|
|
470
|
+
const t = this.tmp("needle");
|
|
471
|
+
const vt = v.literal !== undefined ? U64 : v.type;
|
|
472
|
+
if (!["int", "bool", "pubkey", "string", "enum"].includes(vt.kind))
|
|
473
|
+
this.fail(cb, `${method} compares integers, booleans, Pubkeys, strings or enums`);
|
|
474
|
+
const typed = v.literal !== undefined ? v : this.expect(v, vt, cb);
|
|
475
|
+
this.line(vt.kind === "pubkey" ? `SolPubkey ${t} = *(${typed.c});` : `${valueC(vt)} ${t} = ${typed.c};`);
|
|
476
|
+
this.scopes.at(-1).set(needle, { c: t, type: vt, mutable: false });
|
|
477
|
+
}
|
|
478
|
+
if (method === "find") {
|
|
479
|
+
// Typed from the first element read inside the loop; declared here.
|
|
480
|
+
result = { c: res, type: BOOL };
|
|
481
|
+
this.line(`/* ${res} */`);
|
|
482
|
+
}
|
|
483
|
+
const findDecl = method === "find" ? this.lines.length - 1 : -1;
|
|
484
|
+
const ci = this.tmp("lm_i");
|
|
485
|
+
this.line(`{`);
|
|
486
|
+
this.block(() => {
|
|
487
|
+
this.line(`uint64_t ${ci} = 0;`);
|
|
488
|
+
this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
|
|
489
|
+
this.line(`while (1) {`);
|
|
490
|
+
this.block(() => {
|
|
491
|
+
const n = this.expect(this.expr(length, U64), U64, list);
|
|
492
|
+
this.line(`if (!(${ci} < ${n.c})) break;`);
|
|
493
|
+
if (valueForm) {
|
|
494
|
+
const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
|
|
495
|
+
const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
|
|
496
|
+
this.line(`if (${hit.c}) {`);
|
|
497
|
+
this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${ci};`);
|
|
498
|
+
this.line(` break;`);
|
|
499
|
+
this.line(`}`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes] };
|
|
503
|
+
const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
|
|
504
|
+
if (accId)
|
|
505
|
+
this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
|
|
506
|
+
const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
|
|
507
|
+
const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
|
|
508
|
+
if (method === "forEach") {
|
|
509
|
+
if (v)
|
|
510
|
+
this.fail(cb, "forEach's callback returns nothing");
|
|
511
|
+
}
|
|
512
|
+
else if (method === "reduce") {
|
|
513
|
+
if (!v)
|
|
514
|
+
this.fail(cb, "reduce's callback returns the new accumulator");
|
|
515
|
+
const nv = this.expect(v, accType, cb);
|
|
516
|
+
this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
|
|
517
|
+
}
|
|
518
|
+
else {
|
|
519
|
+
if (!v)
|
|
520
|
+
this.fail(cb, `${method}'s callback returns a boolean`);
|
|
521
|
+
const b = this.expect(v, BOOL, cb);
|
|
522
|
+
if (method === "some")
|
|
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
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
this.line(`${ci}++;`);
|
|
544
|
+
});
|
|
545
|
+
this.line(`}`);
|
|
546
|
+
});
|
|
547
|
+
this.line(`}`);
|
|
548
|
+
if (method === "find") {
|
|
549
|
+
const inner = elemType;
|
|
550
|
+
this.model.options.set(optionKey(inner), inner);
|
|
551
|
+
this.lines[findDecl] = `${" ".repeat(this.indent)}${optC(inner)} ${res};\n${" ".repeat(this.indent)}ts_memzero(&${res}, sizeof(${res}));`;
|
|
552
|
+
return { c: res, type: { kind: "option", inner } };
|
|
553
|
+
}
|
|
554
|
+
return result;
|
|
555
|
+
}
|
|
556
|
+
/** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
|
|
557
|
+
closureReturn(stmt, frame) {
|
|
558
|
+
if (!stmt.expression) {
|
|
559
|
+
if (frame.type)
|
|
560
|
+
this.fail(stmt, "this function returns a value");
|
|
561
|
+
this.line(`goto ${frame.label};`);
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
this.closureResult(this.expr(stmt.expression, frame.type), stmt.expression, frame);
|
|
565
|
+
}
|
|
566
|
+
/** Store a closure's result (fixing its type on the first one) and jump past the rest of its body. */
|
|
567
|
+
closureResult(value, at, frame) {
|
|
568
|
+
let v = value;
|
|
569
|
+
const stmt = { expression: at };
|
|
570
|
+
if (v.isNull && !frame.type)
|
|
571
|
+
this.fail(stmt.expression, "give the function a return type to return null");
|
|
572
|
+
if (!frame.type) {
|
|
573
|
+
if (v.literal !== undefined)
|
|
574
|
+
v = this.literal(v.literal, U64, stmt.expression);
|
|
575
|
+
const t = v.type;
|
|
576
|
+
if (!["int", "bool", "pubkey", "string", "enum", "option"].includes(t.kind))
|
|
577
|
+
this.fail(stmt.expression, `closures return integers, booleans, Pubkeys, strings, enums or Options, not ${typeName(t)}`);
|
|
578
|
+
frame.type = t;
|
|
579
|
+
this.lines[frame.declAt] = `${frame.pad}${t.kind === "pubkey" ? "SolPubkey" : valueC(t)} ${frame.result};`;
|
|
580
|
+
}
|
|
581
|
+
v = this.expect(v, frame.type, stmt.expression);
|
|
582
|
+
if (frame.type.kind === "pubkey")
|
|
583
|
+
this.line(`${frame.result} = *(${v.c});`);
|
|
584
|
+
else if (frame.type.kind === "option" && v.isNull)
|
|
585
|
+
this.line(`ts_memzero(&${frame.result}, sizeof(${frame.result}));`);
|
|
586
|
+
else
|
|
587
|
+
this.line(`${frame.result} = ${this.ownedString(v).c};`);
|
|
588
|
+
this.line(`goto ${frame.label};`);
|
|
589
|
+
}
|
|
590
|
+
/** `do { … } while (cond)`: the body once, then the test; `continue` goes to the test. */
|
|
591
|
+
doLoop(stmt) {
|
|
592
|
+
const label = this.tmp("ts_continue_");
|
|
593
|
+
this.line(`while (1) {`);
|
|
594
|
+
this.block(() => {
|
|
595
|
+
this.loops.push(label);
|
|
596
|
+
this.line(`{`);
|
|
597
|
+
this.block(() => this.body(stmt.statement));
|
|
598
|
+
this.line(`}`);
|
|
599
|
+
this.loops.pop();
|
|
600
|
+
this.line(`${label}:;`);
|
|
601
|
+
const c = this.expect(this.expr(stmt.expression, BOOL), BOOL, stmt.expression);
|
|
602
|
+
this.line(`if (!${c.c}) break;`);
|
|
603
|
+
});
|
|
604
|
+
this.line(`}`);
|
|
605
|
+
}
|
|
276
606
|
body(stmt) {
|
|
277
607
|
if (ts.isBlock(stmt))
|
|
278
608
|
stmt.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
|
|
@@ -290,6 +620,22 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
290
620
|
}
|
|
291
621
|
if (!ts.isIdentifier(d.name) || !d.initializer)
|
|
292
622
|
this.fail(d, "declare one plain name with an initializer");
|
|
623
|
+
// A local function is a helper of its own (lifted when the program was analyzed): nothing to run here.
|
|
624
|
+
if (ts.isArrowFunction(d.initializer) || ts.isFunctionExpression(d.initializer)) {
|
|
625
|
+
// A closure: remembered with the scopes it can see, and expanded where it is called.
|
|
626
|
+
if (this.model.closureDecls.has(d)) {
|
|
627
|
+
this.scopes.at(-1).set(d.name.text, {
|
|
628
|
+
c: "",
|
|
629
|
+
type: BOOL,
|
|
630
|
+
mutable: false,
|
|
631
|
+
closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes] },
|
|
632
|
+
});
|
|
633
|
+
continue;
|
|
634
|
+
}
|
|
635
|
+
if (!this.model.helperDecls.has(d))
|
|
636
|
+
this.fail(d, "declare local functions with const: const f = (…) => …");
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
293
639
|
const name = d.name.text;
|
|
294
640
|
if (this.scopes.at(-1).has(name))
|
|
295
641
|
this.fail(d.name, `"${name}" is already declared`);
|
|
@@ -313,9 +659,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
313
659
|
continue;
|
|
314
660
|
}
|
|
315
661
|
// Struct values: `const p: Point = { x, y }` (or a copy of another Point) is a local of its own.
|
|
316
|
-
const structType = d.type &&
|
|
317
|
-
? this.model.structs.get(d.type.typeName.text)
|
|
318
|
-
: undefined;
|
|
662
|
+
const structType = d.type && structNameOf(d.type, this.scope) ? this.model.structs.get(structNameOf(d.type, this.scope)) : undefined;
|
|
319
663
|
if (structType) {
|
|
320
664
|
if (mutable)
|
|
321
665
|
this.fail(d, "bind struct values with const (their fields can still be updated)");
|
|
@@ -449,8 +793,23 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
449
793
|
const base = compound[op];
|
|
450
794
|
if (base !== undefined)
|
|
451
795
|
return this.assign(e.left, e.right, base);
|
|
452
|
-
if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken)
|
|
453
|
-
|
|
796
|
+
if (op === ts.SyntaxKind.AsteriskAsteriskEqualsToken) {
|
|
797
|
+
// x **= n is x = x ** n; the target is read twice, so it must not run anything (no calls in it).
|
|
798
|
+
let hasCall = false;
|
|
799
|
+
const visit = (n) => {
|
|
800
|
+
if (ts.isCallExpression(n) && !this.isStorageCall(n))
|
|
801
|
+
hasCall = true;
|
|
802
|
+
ts.forEachChild(n, visit);
|
|
803
|
+
};
|
|
804
|
+
visit(e.left);
|
|
805
|
+
if (hasCall)
|
|
806
|
+
this.fail(e.left, "**= on a target that calls a function: write x = x ** n with the target in a local");
|
|
807
|
+
const f = ts.factory;
|
|
808
|
+
const pow = f.createBinaryExpression(e.left, ts.SyntaxKind.AsteriskAsteriskToken, e.right);
|
|
809
|
+
ts.setTextRange(pow, e);
|
|
810
|
+
pow.parent = e;
|
|
811
|
+
return this.assign(e.left, pow);
|
|
812
|
+
}
|
|
454
813
|
}
|
|
455
814
|
if ((ts.isPostfixUnaryExpression(e) || ts.isPrefixUnaryExpression(e)) &&
|
|
456
815
|
(e.operator === ts.SyntaxKind.PlusPlusToken || e.operator === ts.SyntaxKind.MinusMinusToken)) {
|
|
@@ -498,14 +857,15 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
498
857
|
}
|
|
499
858
|
// JS evaluates the target (`a.get(i).x`: which element) before the right side. The exception is
|
|
500
859
|
// `acct.field = acct.create(...)`, which must create the account before the field can be touched.
|
|
501
|
-
|
|
860
|
+
// `fixedVec([…])` is a value written in place, not a call.
|
|
861
|
+
const isCall = ts.isCallExpression(valueNode) && !this.isStorageCall(valueNode) && this.exportName(valueNode.expression) !== "fixedVec";
|
|
502
862
|
const createsFirst = isCall && ts.isPropertyAccessExpression(valueNode.expression) && ["create", "createIfNeeded"].includes(valueNode.expression.name.text);
|
|
503
863
|
let raw = createsFirst ? this.call(valueNode, topLevel) : undefined;
|
|
504
864
|
if (createsFirst && !raw)
|
|
505
865
|
this.fail(valueNode, "this call does not produce a value");
|
|
506
866
|
// A helper's struct result is copied like any struct (the target first, as JS evaluates it first).
|
|
507
867
|
const structCall = isCall && ts.isIdentifier(valueNode.expression)
|
|
508
|
-
? this.
|
|
868
|
+
? this.helperOf(valueNode)?.returns?.kind === "struct"
|
|
509
869
|
: false;
|
|
510
870
|
if (structCall && this.writesThroughVec(target) && this.helperTakesMutAccounts(valueNode))
|
|
511
871
|
this.fail(target, "the right side calls a helper that may change this Vec; store its result in a local first");
|
|
@@ -515,6 +875,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
515
875
|
(targetRef.type.kind === "array" ||
|
|
516
876
|
targetRef.type.kind === "struct" ||
|
|
517
877
|
targetRef.type.kind === "dataEnum" ||
|
|
878
|
+
(targetRef.type.kind === "vec" && targetRef.type.cap !== undefined) ||
|
|
518
879
|
(targetRef.type.kind === "option" && targetRef.type.inner.kind === "struct"))) {
|
|
519
880
|
if (!targetRef.writable)
|
|
520
881
|
this.fail(target, this.readOnly(targetRef));
|
|
@@ -554,7 +915,7 @@ export class StatementEmitter extends CpiEmitter {
|
|
|
554
915
|
}
|
|
555
916
|
}
|
|
556
917
|
helperTakesMutAccounts(node) {
|
|
557
|
-
const h =
|
|
918
|
+
const h = this.helperOf(node);
|
|
558
919
|
return !!h?.params.some((p) => p.account && isWritable(p.account));
|
|
559
920
|
}
|
|
560
921
|
}
|
|
@@ -66,7 +66,13 @@ export declare abstract class StorageEmitter extends EmitterBase {
|
|
|
66
66
|
* Write a struct or array into storage: from an object literal (field by field, every field
|
|
67
67
|
* required) or by copying another location of the same type.
|
|
68
68
|
*/
|
|
69
|
+
/** A whole struct account copied as a value (`return vault`, `info = vault`): fixed-size structs only. */
|
|
70
|
+
private wholeAccount;
|
|
69
71
|
protected writeAggregate(target: Ref, valueNode: ts.Expression): void;
|
|
72
|
+
/** The Vec view of a FixedVec stored at `lv` (`{ len, items[cap] }`). */
|
|
73
|
+
protected fixedVec(lv: string, elem: FieldType, cap: number): NonNullable<Ref["vec"]>;
|
|
74
|
+
/** A place whose type is a FixedVec gets its Vec view. */
|
|
75
|
+
protected withFixedVec(r: Ref): Ref;
|
|
70
76
|
/** `arr.get(i)` reads storage rather than calling anything. */
|
|
71
77
|
protected isStorageCall(node: ts.CallExpression): boolean;
|
|
72
78
|
/** Whether an expression names an array, without emitting code or failing. */
|