@typesol/cli 0.22.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.
Files changed (40) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/README.md +13 -12
  3. package/SECURITY.md +1 -1
  4. package/dist/clientgen.js +190 -41
  5. package/dist/compiler/analyze.js +195 -25
  6. package/dist/compiler/emit/base.d.ts +28 -5
  7. package/dist/compiler/emit/base.js +81 -15
  8. package/dist/compiler/emit/calls.d.ts +18 -1
  9. package/dist/compiler/emit/calls.js +159 -7
  10. package/dist/compiler/emit/cpi.d.ts +23 -0
  11. package/dist/compiler/emit/cpi.js +274 -5
  12. package/dist/compiler/emit/expressions.d.ts +9 -1
  13. package/dist/compiler/emit/expressions.js +25 -12
  14. package/dist/compiler/emit/function.js +8 -4
  15. package/dist/compiler/emit/program.js +9 -2
  16. package/dist/compiler/emit/shared.d.ts +43 -3
  17. package/dist/compiler/emit/shared.js +43 -0
  18. package/dist/compiler/emit/statements.d.ts +39 -2
  19. package/dist/compiler/emit/statements.js +346 -87
  20. package/dist/compiler/emit/storage.d.ts +6 -2
  21. package/dist/compiler/emit/storage.js +75 -45
  22. package/dist/compiler/model.d.ts +26 -1
  23. package/dist/idlpublish.d.ts +6 -1
  24. package/dist/idlpublish.js +47 -5
  25. package/dist/portgen.js +1 -1
  26. package/dist/svmrpc.d.ts +7 -1
  27. package/dist/svmrpc.js +133 -19
  28. package/dist/testing.js +23 -3
  29. package/docs/cli.md +2 -2
  30. package/docs/client.md +13 -10
  31. package/docs/errors.md +9 -3
  32. package/docs/language.md +21 -20
  33. package/docs/mainnet.md +1 -1
  34. package/docs/migrate-from-anchor.md +8 -9
  35. package/docs/test-your-program.md +6 -2
  36. package/docs/testing.md +1 -1
  37. package/docs/troubleshooting.md +3 -2
  38. package/lib/typesol.d.ts +127 -20
  39. package/package.json +1 -1
  40. package/runtime/typesol_rt.h +73 -18
@@ -1,7 +1,7 @@
1
1
  import ts from "typescript";
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";
2
+ import { MAX_RETURN_DATA, calleeName, resolveScalarType, resolveValueType, structNameOf, typesolName } from "../analyze.js";
3
+ import { INT_INFO, isScalar, isWritable, optionKey, scalarSize, typeName, } from "../model.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,234 @@ export class StatementEmitter extends CpiEmitter {
259
259
  n.parent = stmt;
260
260
  return n;
261
261
  };
262
- const indexId = place(f.createIdentifier(index));
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
+ /** The function an identifier names (a closure, a local function or a helper), as something to expand; else undefined. */
280
+ functionNamed(id) {
281
+ const l = this.lookup(id.text);
282
+ if (l)
283
+ return l.closure;
284
+ const name = calleeName(id, this.model.helperDecls, this.checker);
285
+ const h = name ? (this.model.helpers.get(name) ?? this.model.generics.get(name)) : undefined;
286
+ return h ? { node: h.node, name: h.display, scopes: [], env: this.noEnv(), imports: h.imports } : undefined;
287
+ }
288
+ /** `xs.map(…)` / `xs.filter(…)` (or a chain of them): a list built element by element, not stored anywhere yet. */
289
+ isPipeline(e) {
290
+ return ts.isCallExpression(e) && ts.isPropertyAccessExpression(e.expression) && (e.expression.name.text === "map" || e.expression.name.text === "filter");
291
+ }
292
+ /** The list under a chain of map / filter calls, and the calls in the order they run. */
293
+ pipelineStages(e) {
294
+ const stages = [];
295
+ let base = e;
296
+ while (this.isPipeline(base)) {
297
+ const access = base.expression;
298
+ const kind = access.name.text;
299
+ if (base.arguments.length !== 1)
300
+ this.fail(base, `${kind}() takes one argument: the callback`);
301
+ const fn = base.arguments[0];
302
+ if (!ts.isArrowFunction(fn) && !ts.isFunctionExpression(fn))
303
+ this.fail(fn, `pass ${kind} an arrow function written here: xs.${kind}((x) => …)`);
304
+ if (fn.parameters.length > 2)
305
+ this.fail(fn.parameters[2], `${kind}'s callback takes the element and its index`);
306
+ stages.unshift({ kind, fn });
307
+ base = access.expression;
308
+ }
309
+ if (stages.length) {
310
+ const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
311
+ if (!(l?.type.kind === "vecArg" || l?.type.kind === "array" || this.peekArray(base)))
312
+ this.fail(base, "map and filter work on a Vec, FixedArray or FixedVec");
313
+ }
314
+ return { base, stages };
315
+ }
316
+ /**
317
+ * Run `body` for each element of a list, or of a chain of `map` / `filter` calls on one (`xs.filter(f).map(g)`):
318
+ * one index loop over the list (its length read every step, as `for … of`), each callback expanded in it. A
319
+ * filter skips to the next element, a map binds its result, so a chain builds no list. `body` gets the element (as
320
+ * `list.get(i)` gives it, structs by reference, or the mapped value) and its index in the chain's output (after a
321
+ * filter, the count of elements it let through, as JS gives callbacks).
322
+ */
323
+ eachElement(list, at, names, body) {
324
+ const f = ts.factory;
325
+ const place = (n) => {
326
+ ts.setTextRange(n, at);
327
+ n.parent = at;
328
+ return n;
329
+ };
330
+ const { base, stages } = this.pipelineStages(list);
331
+ const { index, ci } = names;
332
+ const indexId = place(f.createIdentifier(index));
333
+ const length = place(f.createPropertyAccessExpression(base, "length"));
334
+ const get = place(f.createCallExpression(place(f.createPropertyAccessExpression(base, "get")), undefined, [indexId]));
335
+ const next = names.next ?? (stages.some((s) => s.kind === "filter") ? this.tmp("ts_next_") : undefined);
272
336
  this.line(`{`);
273
337
  this.block(() => {
274
338
  this.line(`uint64_t ${ci} = 0;`);
275
339
  this.scopes.at(-1).set(index, { c: ci, type: U64, mutable: false });
340
+ const counters = stages
341
+ .filter((st) => st.kind === "filter")
342
+ .map(() => {
343
+ const k = this.tmp("lm_k");
344
+ this.line(`uint64_t ${k} = 0;`);
345
+ return k;
346
+ });
276
347
  this.line(`while (1) {`);
277
348
  this.block(() => {
278
- const n = this.expect(this.expr(length, U64), U64, stmt.expression);
349
+ const n = this.expect(this.expr(length, U64), U64, base);
279
350
  this.line(`if (!(${ci} < ${n.c})) break;`);
280
- this.variable(declList);
281
- this.loops.push(label);
282
- this.line(`{`);
283
- this.block(() => this.body(stmt.statement));
284
- this.line(`}`);
285
- this.loops.pop();
286
- this.line(`${label}:;`);
351
+ let element = get;
352
+ let idx = indexId;
353
+ let k = 0;
354
+ for (const st of stages) {
355
+ if (st.kind === "map") {
356
+ const ret = st.fn.body && !ts.isBlock(st.fn.body) ? st.fn.body : undefined;
357
+ const bare = ret && ts.isParenthesizedExpression(ret) ? ret.expression : ret;
358
+ if (bare && ts.isObjectLiteralExpression(bare))
359
+ this.fail(st.fn, "a map to structs is stored, with the struct type: const ys: FixedVec<MyStruct, N> = xs.map((x) => ({ … }))");
360
+ }
361
+ const closure = { node: st.fn, name: `${st.kind}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
362
+ const v = this.expandClosure(closure, [element, idx].slice(0, st.fn.parameters.length), st.fn);
363
+ if (st.kind === "filter") {
364
+ if (!v)
365
+ this.fail(st.fn, "filter's callback returns a boolean");
366
+ const b = this.expect(v, BOOL, st.fn);
367
+ this.line(`if (!(${b.c})) goto ${next};`);
368
+ const id = `__lk${this.temp++}`;
369
+ const ic = this.tmp("lm_ki");
370
+ this.line(`uint64_t ${ic} = ${counters[k++]}++;`);
371
+ this.scopes.at(-1).set(id, { c: ic, type: U64, mutable: false });
372
+ idx = place(f.createIdentifier(id));
373
+ }
374
+ else {
375
+ if (!v)
376
+ this.fail(st.fn, "map's callback returns the new element");
377
+ const mv = v.literal !== undefined ? this.literal(v.literal, U64, st.fn) : v;
378
+ if (!["int", "bool", "pubkey", "enum"].includes(mv.type.kind))
379
+ this.fail(st.fn, `map's callback returns integers, booleans, Pubkeys or enums (the new list's elements), not ${typeName(mv.type)}`);
380
+ const id = `__lv${this.temp++}`;
381
+ const mc = this.tmp("lm_v");
382
+ this.line(mv.type.kind === "pubkey" ? `SolPubkey ${mc} = *(${mv.c});` : `${valueC(mv.type)} ${mc} = ${mv.c};`);
383
+ this.scopes.at(-1).set(id, { c: mc, type: mv.type, mutable: false });
384
+ element = place(f.createIdentifier(id));
385
+ }
386
+ }
387
+ body(element, idx);
388
+ if (next && (names.alwaysNext || counters.length))
389
+ this.line(`${next}:;`);
287
390
  this.line(`${ci}++;`);
288
391
  });
289
392
  this.line(`}`);
290
393
  });
291
394
  this.line(`}`);
292
395
  }
396
+ /**
397
+ * `const ys = xs.map(…)` / `xs.filter(…)`: the chain's elements in a local FixedVec, as big as the list it starts
398
+ * from (a FixedArray's length, a FixedVec's capacity) or as annotated (`const ys: FixedVec<u64, 16> = …`, needed
399
+ * 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.
400
+ */
401
+ localList(d, init, c) {
402
+ const { base, stages } = this.pipelineStages(init);
403
+ let cap;
404
+ let annotatedElem;
405
+ if (d.type) {
406
+ const t = d.type;
407
+ const args = ts.isTypeReferenceNode(t) ? (t.typeArguments ?? []) : [];
408
+ const n = args[1] && ts.isLiteralTypeNode(args[1]) && ts.isNumericLiteral(args[1].literal) ? Number(args[1].literal.text) : NaN;
409
+ const elemNode = args[0];
410
+ const sn = elemNode && structNameOf(elemNode, this.scope);
411
+ const fs = elemNode && typesolName(elemNode, this.scope.imports) === "FixedString" ? elemNode.typeArguments?.[0] : undefined;
412
+ const fcap = fs && ts.isLiteralTypeNode(fs) && ts.isNumericLiteral(fs.literal) ? Number(fs.literal.text) : NaN;
413
+ if (Number.isInteger(fcap) && fcap >= 1)
414
+ this.model.fstrings.add(fcap);
415
+ annotatedElem = elemNode
416
+ ? (resolveScalarType(elemNode, this.scope) ??
417
+ (sn && this.model.structs.has(sn) ? { kind: "struct", name: sn } : Number.isInteger(fcap) && fcap >= 1 ? { kind: "fstring", cap: fcap } : undefined))
418
+ : undefined;
419
+ if (!ts.isTypeReferenceNode(t) || typesolName(t, this.scope.imports) !== "FixedVec" || !annotatedElem || !Number.isInteger(n) || n < 1)
420
+ this.fail(t, "annotate the new list as FixedVec<T, N> (N a literal)");
421
+ cap = n;
422
+ }
423
+ else {
424
+ const r = this.reporter.guard(() => this.ref(base));
425
+ const l = ts.isIdentifier(base) ? this.lookup(base.text) : undefined;
426
+ if (r?.type.kind === "array")
427
+ cap = r.type.len;
428
+ else if (r?.type.kind === "vec" && r.type.cap !== undefined)
429
+ cap = r.type.cap;
430
+ if (cap === undefined || l?.type.kind === "vecArg")
431
+ this.fail(init, "this list has no fixed size: annotate the new one with its capacity, const ys: FixedVec<T, N> = …");
432
+ }
433
+ // The element type: the last map's result, else the list's own elements (known once the loop is emitted).
434
+ const declAt = this.lines.length;
435
+ this.line(`/* ${c} */`);
436
+ let elem = stages.some((st) => st.kind === "map") ? undefined : this.listElem(base);
437
+ const cap0 = cap;
438
+ // A last map to structs or FixedStrings (annotated) writes each result straight into its slot.
439
+ const last = stages.at(-1);
440
+ if (last?.kind === "map" && annotatedElem && !isScalar(annotatedElem)) {
441
+ const target = annotatedElem;
442
+ const inner = init.expression.expression;
443
+ this.eachElement(inner, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element, index) => {
444
+ this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
445
+ const closure = { node: last.fn, name: "map's callback", scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
446
+ this.expandClosure(closure, [element, index].slice(0, last.fn.parameters.length), last.fn, {
447
+ lv: `${c}.items[${c}.len]`,
448
+ type: target,
449
+ writable: true,
450
+ });
451
+ this.line(`${c}.len++;`);
452
+ });
453
+ elem = target;
454
+ }
455
+ else
456
+ this.eachElement(init, init, { index: `__ml${this.temp++}`, ci: this.tmp("ml_i") }, (element) => {
457
+ if (!elem) {
458
+ const v = this.expr(element);
459
+ elem = v.type;
460
+ }
461
+ const target = annotatedElem ?? elem;
462
+ const sub = { lv: `${c}.items[${c}.len]`, type: target, writable: true };
463
+ this.line(`if (${c}.len >= ${cap0}) TS_FAIL(TS_ERR_INDEX, "TypeSOL: the new list is full");`);
464
+ const vt = this.storageValueType(target);
465
+ if (vt)
466
+ this.line(this.writeStorage(sub, this.ownedString(this.expect(this.expr(element, vt), vt, element)).c) + ";");
467
+ else
468
+ this.writeAggregate(sub, element);
469
+ this.line(`${c}.len++;`);
470
+ });
471
+ const t = { kind: "vec", elem: annotatedElem ?? elem, cap: cap0 };
472
+ const bytes = 4 + this.fieldBytes(t.elem) * cap0;
473
+ if (bytes > 2048)
474
+ 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>)`);
475
+ this.model.localVecs.set(fixedVecC(t), t);
476
+ const pad = " ".repeat(this.indent);
477
+ this.lines[declAt] = `${pad}${fixedVecC(t)} ${c};\n${pad}ts_memzero(&${c}, sizeof(${c}));`;
478
+ return t;
479
+ }
480
+ /** The element type of a Vec, FixedArray, FixedVec or Vec argument. */
481
+ listElem(list) {
482
+ const l = ts.isIdentifier(list) ? this.lookup(list.text) : undefined;
483
+ if (l?.type.kind === "vecArg")
484
+ return l.type.elem;
485
+ const r = this.ref(list);
486
+ if (r && (r.type.kind === "array" || r.type.kind === "vec"))
487
+ return r.type.elem;
488
+ this.fail(list, "expected a Vec, FixedArray or FixedVec");
489
+ }
293
490
  /** Whether `e` is the remaining accounts (or a const bound to them). */
294
491
  isRemaining(e) {
295
492
  if (ts.isCallExpression(e) && this.exportName(e.expression) === "remainingAccounts")
@@ -325,24 +522,43 @@ export class StatementEmitter extends CpiEmitter {
325
522
  * was declared with, so a block around the call that declares the same name does not change what it reads, and
326
523
  * writes to captured `let`s are seen by the enclosing function. Returns the result (undefined for no value).
327
524
  */
328
- expandClosure(c, args, node) {
525
+ expandClosure(c, args, node, into) {
329
526
  const fn = c.node;
330
527
  if (this.inlining.has(fn))
331
528
  this.fail(node, `${c.name} calls itself: closures cannot be recursive`);
529
+ // Its types are written in its own file: they mean what that file's imports say (the arguments are the caller's).
530
+ const calleeScope = c.imports === this.scope.imports ? this.scope : { ...this.scope, imports: c.imports };
332
531
  if (args.length !== fn.parameters.length)
333
532
  this.fail(node, `${c.name}() takes ${fn.parameters.length} argument(s)`);
334
533
  const params = new Map();
534
+ // Account parameters (of a helper expanded here) name the caller's accounts while the body runs.
535
+ const aliases = new Map();
335
536
  fn.parameters.forEach((p, i) => {
336
537
  if (!ts.isIdentifier(p.name) || p.initializer || p.dotDotDotToken)
337
538
  this.fail(p, "closure parameters are plain names");
338
539
  const arg = args[i];
339
540
  const name = p.name.text;
541
+ // A function parameter (of a helper expanded here): the function given, expanded where the body calls it.
542
+ if (p.type && ts.isFunctionTypeNode(p.type)) {
543
+ params.set(name, { c: "", type: BOOL, mutable: false, closure: this.closureOf(arg) });
544
+ return;
545
+ }
546
+ if (p.type && ts.isIdentifier(arg) && !this.lookup(arg.text) && this.accounts.has(arg.text) && !resolveValueType(p.type, calleeScope)) {
547
+ aliases.set(name, this.accounts.get(arg.text));
548
+ return;
549
+ }
340
550
  const c2 = `l_${name}_${this.temp++}`;
341
- const annotated = p.type ? resolveValueType(p.type, this.scope) : undefined;
551
+ const annotated = p.type ? resolveValueType(p.type, calleeScope) : undefined;
342
552
  if (!annotated && this.looksLikeStructPlace(arg)) {
343
553
  const place = this.structPlace(arg);
344
554
  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) });
555
+ params.set(name, {
556
+ c: c2,
557
+ type: { kind: "structRef", struct: place.struct, writable: place.writable },
558
+ mutable: false,
559
+ paths: this.pathsOf(arg),
560
+ decl: p,
561
+ });
346
562
  return;
347
563
  }
348
564
  let v = this.expr(arg, annotated);
@@ -358,7 +574,7 @@ export class StatementEmitter extends CpiEmitter {
358
574
  this.line(`${valueC(type)}${type.kind === "account" ? "" : " "}${c2} = ${this.ownedString(v).c};`);
359
575
  params.set(name, { c: c2, type, mutable: true });
360
576
  });
361
- const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, this.scope) : undefined;
577
+ const declared = fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword ? resolveValueType(fn.type, calleeScope) : undefined;
362
578
  if (fn.type && fn.type.kind !== ts.SyntaxKind.VoidKeyword && !declared)
363
579
  this.fail(fn.type, "closures return integers, booleans, Pubkeys, strings, enums or Options");
364
580
  const frame = {
@@ -368,16 +584,42 @@ export class StatementEmitter extends CpiEmitter {
368
584
  declAt: this.lines.length,
369
585
  pad: " ".repeat(this.indent),
370
586
  node: fn,
587
+ into,
371
588
  };
372
589
  this.line(declared ? `${declared.kind === "pubkey" ? "SolPubkey" : valueC(declared)} ${frame.result};` : `/* ${frame.result} */`);
373
- const saved = { scopes: this.scopes, loops: this.loops.splice(0) };
590
+ // The body names what it could where it was written: a closure the caller's arguments and accounts, a helper
591
+ // only its own parameters (its account parameters name the caller's accounts) and the module.
592
+ const swap = (m, to) => {
593
+ const before = new Map(m);
594
+ m.clear();
595
+ for (const [k, v] of to)
596
+ m.set(k, v);
597
+ return before;
598
+ };
599
+ const bodyAccounts = new Map(c.env.accounts);
600
+ for (const [n, a] of aliases)
601
+ bodyAccounts.set(n, a);
602
+ // Accounts are found by name before locals: a parameter named like one the body can see would read the account.
603
+ for (const n of params.keys())
604
+ if (bodyAccounts.has(n))
605
+ this.fail(fn.parameters.find((p) => ts.isIdentifier(p.name) && p.name.text === n) ?? node, `"${n}" shadows the account "${n}"; use another name`);
606
+ const saved = {
607
+ scope: this.scope,
608
+ scopes: this.scopes,
609
+ loops: this.loops.splice(0),
610
+ args: swap(this.args, c.env.args),
611
+ accounts: swap(this.accounts, bodyAccounts),
612
+ };
374
613
  this.scopes = [...c.scopes, params];
614
+ this.scope = calleeScope;
375
615
  this.inlining.add(fn);
376
616
  this.inlineFrames.push(frame);
377
617
  try {
378
618
  this.line(`{`);
379
619
  this.block(() => {
380
620
  const body = fn.body;
621
+ if (!body)
622
+ this.fail(fn, `${c.name} has no body`);
381
623
  if (ts.isBlock(body))
382
624
  body.statements.forEach((s) => this.reporter.guard(() => this.statement(s)));
383
625
  else {
@@ -385,7 +627,7 @@ export class StatementEmitter extends CpiEmitter {
385
627
  const ret = ts.factory.createReturnStatement(body);
386
628
  ts.setTextRange(ret, body);
387
629
  ret.parent = fn;
388
- if (ts.isCallExpression(body) && !frame.type) {
630
+ if (ts.isCallExpression(body) && !frame.type && !into) {
389
631
  const v = this.call(body, false);
390
632
  if (v)
391
633
  this.closureResult(v, body, frame);
@@ -400,7 +642,10 @@ export class StatementEmitter extends CpiEmitter {
400
642
  finally {
401
643
  this.inlineFrames.pop();
402
644
  this.inlining.delete(fn);
645
+ swap(this.args, saved.args);
646
+ swap(this.accounts, saved.accounts);
403
647
  this.scopes = saved.scopes;
648
+ this.scope = saved.scope;
404
649
  this.loops.push(...saved.loops);
405
650
  }
406
651
  if (!frame.type) {
@@ -434,9 +679,7 @@ export class StatementEmitter extends CpiEmitter {
434
679
  this.fail(cb, `pass ${method} an arrow function written here: xs.${method}((x) => …)`);
435
680
  const fn = cb;
436
681
  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]));
682
+ void place;
440
683
  const I64 = { kind: "int", name: "i64" };
441
684
  // The result, declared before the loop.
442
685
  const res = this.tmp("lm");
@@ -482,69 +725,58 @@ export class StatementEmitter extends CpiEmitter {
482
725
  }
483
726
  const findDecl = method === "find" ? this.lines.length - 1 : -1;
484
727
  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(`}`);
728
+ this.eachElement(list, node, { index, ci }, (element, indexId) => {
729
+ if (valueForm) {
730
+ const eq = place(f.createBinaryExpression(element, ts.SyntaxKind.EqualsEqualsEqualsToken, place(f.createIdentifier(needle))));
731
+ const hit = this.expect(this.expr(eq, BOOL), BOOL, node);
732
+ this.line(`if (${hit.c}) {`);
733
+ this.line(method === "includes" ? ` ${res} = true;` : ` ${res} = (int64_t)${this.expr(indexId, U64).c};`);
734
+ this.line(` break;`);
735
+ this.line(`}`);
736
+ }
737
+ else {
738
+ const closure = { node: fn, name: `${method}'s callback`, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports };
739
+ const accId = method === "reduce" ? place(f.createIdentifier(`__acc${this.temp++}`)) : undefined;
740
+ if (accId)
741
+ this.scopes.at(-1).set(accId.text, { c: res, type: accType, mutable: true });
742
+ const all = method === "reduce" ? [accId, element, indexId] : [element, indexId];
743
+ const v = this.expandClosure(closure, all.slice(0, fn.parameters.length), cb);
744
+ if (method === "forEach") {
745
+ if (v)
746
+ this.fail(cb, "forEach's callback returns nothing");
747
+ }
748
+ else if (method === "reduce") {
749
+ if (!v)
750
+ this.fail(cb, "reduce's callback returns the new accumulator");
751
+ const nv = this.expect(v, accType, cb);
752
+ this.line(accType.kind === "pubkey" ? `${res} = *(${nv.c});` : `${res} = ${nv.c};`);
500
753
  }
501
754
  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
- }
755
+ if (!v)
756
+ this.fail(cb, `${method}'s callback returns a boolean`);
757
+ const b = this.expect(v, BOOL, cb);
758
+ if (method === "some")
759
+ this.line(`if (${b.c}) { ${res} = true; break; }`);
760
+ if (method === "every")
761
+ this.line(`if (!(${b.c})) { ${res} = false; break; }`);
762
+ if (method === "findIndex")
763
+ this.line(`if (${b.c}) { ${res} = (int64_t)${this.expr(indexId, U64).c}; break; }`);
764
+ if (method === "find") {
765
+ this.line(`if (${b.c}) {`);
766
+ this.block(() => {
767
+ const e = this.expr(element);
768
+ elemType = e.type;
769
+ if (!["int", "bool", "pubkey", "enum"].includes(e.type.kind))
770
+ this.fail(node, "find returns an Option of the element, so the elements must be integers, booleans, Pubkeys or enums; use findIndex");
771
+ this.line(`${res}.some = 1;`);
772
+ this.line(e.type.kind === "pubkey" ? `${res}.v = *(${e.c});` : `${res}.v = ${e.c};`);
773
+ this.line(`break;`);
774
+ });
775
+ this.line(`}`);
541
776
  }
542
777
  }
543
- this.line(`${ci}++;`);
544
- });
545
- this.line(`}`);
778
+ }
546
779
  });
547
- this.line(`}`);
548
780
  if (method === "find") {
549
781
  const inner = elemType;
550
782
  this.model.options.set(optionKey(inner), inner);
@@ -555,6 +787,17 @@ export class StatementEmitter extends CpiEmitter {
555
787
  }
556
788
  /** `return` inside an expanded closure: the value goes to the closure's result, then past its body. */
557
789
  closureReturn(stmt, frame) {
790
+ if (frame.into && stmt.expression) {
791
+ // Written straight into its place (an object literal for a struct, text for a FixedString, or a value).
792
+ const target = frame.into;
793
+ const vt = this.storageValueType(target.type);
794
+ if (vt)
795
+ this.line(this.writeStorage(target, this.ownedString(this.expect(this.expr(stmt.expression, vt), vt, stmt.expression)).c) + ";");
796
+ else
797
+ this.writeAggregate(target, stmt.expression);
798
+ this.line(`goto ${frame.label};`);
799
+ return;
800
+ }
558
801
  if (!stmt.expression) {
559
802
  if (frame.type)
560
803
  this.fail(stmt, "this function returns a value");
@@ -628,7 +871,7 @@ export class StatementEmitter extends CpiEmitter {
628
871
  c: "",
629
872
  type: BOOL,
630
873
  mutable: false,
631
- closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes] },
874
+ closure: { node: d.initializer, name: d.name.text, scopes: [...this.scopes], env: this.envNow(), imports: this.scope.imports },
632
875
  });
633
876
  continue;
634
877
  }
@@ -644,12 +887,26 @@ export class StatementEmitter extends CpiEmitter {
644
887
  this.fail(d.name, `"${name}" shadows the parameter "${name}"; use another name`);
645
888
  const c = `l_${name}_${this.temp++}`;
646
889
  const init = d.initializer;
890
+ // A function as a value: another name for one (`const h = g`), or the one a function returns (`adder(5n)`).
891
+ const made = ts.isCallExpression(init) ? this.madeFunction(init) : undefined;
892
+ const alias = ts.isIdentifier(init) ? this.functionNamed(init) : undefined;
893
+ if (made || alias) {
894
+ if (mutable)
895
+ this.fail(d, "bind functions with const");
896
+ this.scopes.at(-1).set(name, { c: "", type: BOOL, mutable: false, closure: (made ?? alias) });
897
+ continue;
898
+ }
647
899
  const fn = ts.isCallExpression(init) ? this.exportName(init.expression) : undefined;
648
900
  if (fn === "fixedArray") {
649
901
  const t = this.localArray(init, c);
650
902
  this.scopes.at(-1).set(name, { c, type: t, mutable });
651
903
  continue;
652
904
  }
905
+ if (this.isPipeline(init)) {
906
+ const t = this.localList(d, init, c);
907
+ this.scopes.at(-1).set(name, { c, type: t, mutable });
908
+ continue;
909
+ }
653
910
  if (fn === "loadAccount" || fn === "loadAccountMut" || fn === "loadForeign") {
654
911
  if (mutable)
655
912
  this.fail(d, "bind loaded accounts with const");
@@ -851,6 +1108,8 @@ export class StatementEmitter extends CpiEmitter {
851
1108
  const pieces = this.templatePieces(rhsNode);
852
1109
  if (pieces.text !== undefined)
853
1110
  this.line(`TS_TRY(ts_str_append(&${cur}, ts_str((const uint8_t *)${cString(pieces.text)}, ${Buffer.byteLength(pieces.text, "utf8")})));`);
1111
+ else if (pieces.only !== undefined)
1112
+ this.line(`TS_TRY(ts_str_append(&${cur}, ${pieces.only}));`);
854
1113
  else
855
1114
  this.line(`TS_TRY(ts_str_append_parts(&${cur}, ${pieces.arr}, ${pieces.n}));`);
856
1115
  return;
@@ -27,8 +27,12 @@ export declare abstract class StorageEmitter extends EmitterBase {
27
27
  * value. Refuse when a live reference into the replaced place is still used afterwards.
28
28
  */
29
29
  protected assertNotAliased(replaced: string[] | undefined, at: ts.Node, helper?: string): void;
30
- /** Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`). */
31
- protected usedAfter(decl: ts.VariableDeclaration, at: ts.Node): boolean;
30
+ /**
31
+ * Whether the variable declared by `decl` is read after `at` (later in its block, or anywhere in a loop around `at`).
32
+ * A parameter (of a closure or helper expanded here) is read after `at` when its function reads it after `at`; when
33
+ * `at` is in another function (a closure or helper it calls), any read of it in its own function counts.
34
+ */
35
+ protected usedAfter(decl: ts.VariableDeclaration | ts.ParameterDeclaration, at: ts.Node): boolean;
32
36
  /**
33
37
  * A pointer to a struct in storage, from `place` or `cond ? placeA : placeB` (same struct type).
34
38
  * Returns undefined when the expression is not such a place.