@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.
@@ -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) || ts.isForInStatement(stmt))
69
- this.fail(stmt, "only for (init; cond; step), for (const x of list) and while loops are supported");
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.line(`sol_set_return_data((const uint8_t *)&${t}, sizeof(${t}));`);
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
- if (typed.type.kind === "int" && INT_INFO[typed.type.name].bytes === 16)
150
- this.fail(stmt.expression, "switch on 128-bit integers is not supported; use if/else");
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.line(`switch (${t}) {`);
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
- this.line(`case ${label.c}: {`);
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 && ts.isTypeReferenceNode(d.type) && ts.isIdentifier(d.type.typeName) && !d.type.typeArguments
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
- this.fail(e.operatorToken, "write x = x ** n instead of **=");
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
- const isCall = ts.isCallExpression(valueNode) && !this.isStorageCall(valueNode);
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.model.helpers.get(valueNode.expression.text)?.returns?.kind === "struct"
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 = ts.isIdentifier(node.expression) ? this.model.helpers.get(node.expression.text) : undefined;
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. */