@harmoniclabs/pebble 0.2.0 → 0.3.1

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 (117) hide show
  1. package/dist/IR/CompilationCtx.d.ts +40 -0
  2. package/dist/IR/CompilationCtx.js +54 -0
  3. package/dist/IR/IRHash.d.ts +23 -2
  4. package/dist/IR/IRHash.js +10 -60
  5. package/dist/IR/IRNodes/IRConst.js +22 -2
  6. package/dist/IR/IRNodes/IRHoisted.d.ts +0 -1
  7. package/dist/IR/IRNodes/IRHoisted.js +4 -6
  8. package/dist/IR/IRNodes/IRLetted.d.ts +0 -1
  9. package/dist/IR/IRNodes/IRLetted.js +4 -6
  10. package/dist/IR/IRNodes/IRNative/IRNativeTag.d.ts +22 -2
  11. package/dist/IR/IRNodes/IRNative/IRNativeTag.js +26 -2
  12. package/dist/IR/IRNodes/IRNative/index.d.ts +16 -1
  13. package/dist/IR/IRNodes/IRNative/index.js +27 -2
  14. package/dist/IR/IRNodes/utils/hashVarSym.d.ts +0 -1
  15. package/dist/IR/IRNodes/utils/hashVarSym.js +27 -33
  16. package/dist/IR/toUPLC/CompilerOptions.d.ts +12 -0
  17. package/dist/IR/toUPLC/CompilerOptions.js +14 -9
  18. package/dist/IR/toUPLC/compileIRToUPLC.js +39 -3
  19. package/dist/IR/toUPLC/subRoutines/inlineSingleUseLetBindingsAndReturnRoot.d.ts +23 -0
  20. package/dist/IR/toUPLC/subRoutines/inlineSingleUseLetBindingsAndReturnRoot.js +263 -0
  21. package/dist/IR/toUPLC/subRoutines/introduceCaseForDualHeadTailAndReturnRoot.d.ts +35 -0
  22. package/dist/IR/toUPLC/subRoutines/introduceCaseForDualHeadTailAndReturnRoot.js +169 -0
  23. package/dist/IR/toUPLC/subRoutines/replaceHoistedWithLetted.d.ts +0 -1
  24. package/dist/IR/toUPLC/subRoutines/replaceHoistedWithLetted.js +6 -6
  25. package/dist/IR/toUPLC/subRoutines/replaceNatives/nativeToIR.d.ts +2 -3
  26. package/dist/IR/toUPLC/subRoutines/replaceNatives/nativeToIR.js +106 -65
  27. package/dist/IR/toUPLC/subRoutines/rewriteHeadTailInCaseConsAndReturnRoot.d.ts +30 -0
  28. package/dist/IR/toUPLC/subRoutines/rewriteHeadTailInCaseConsAndReturnRoot.js +95 -0
  29. package/dist/IR/toUPLC/subRoutines/rewriteNativesAppliedToConstantsAndReturnRoot.js +36 -5
  30. package/dist/IR/toUPLC/subRoutines/rewriteToCaseOverConstAndReturnRoot.d.ts +35 -0
  31. package/dist/IR/toUPLC/subRoutines/rewriteToCaseOverConstAndReturnRoot.js +169 -0
  32. package/dist/IR/tree_utils/_ir_caseList.d.ts +15 -0
  33. package/dist/IR/tree_utils/_ir_caseList.js +19 -0
  34. package/dist/ast/nodes/statements/declarations/StructDecl.d.ts +16 -2
  35. package/dist/ast/nodes/statements/declarations/StructDecl.js +15 -1
  36. package/dist/compiler/AstCompiler/AstCompiler.d.ts +1 -0
  37. package/dist/compiler/AstCompiler/AstCompiler.js +41 -4
  38. package/dist/compiler/AstCompiler/internal/_deriveContractBody/_deriveContractBody.js +3 -3
  39. package/dist/compiler/AstCompiler/internal/exprs/_compileCallExpr.js +57 -10
  40. package/dist/compiler/AstCompiler/internal/exprs/_compileCaseExpr.js +31 -0
  41. package/dist/compiler/AstCompiler/internal/exprs/_compileIsExpr.js +12 -0
  42. package/dist/compiler/AstCompiler/internal/exprs/_compilePropAccessExpr.js +36 -0
  43. package/dist/compiler/AstCompiler/internal/exprs/_compileUnaryPrefixExpr.js +13 -1
  44. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileAddExpr.js +18 -5
  45. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileEqualExpr.js +3 -1
  46. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileGreaterThanEqualExpr.js +2 -1
  47. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileGreaterThanExpr.js +2 -1
  48. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileLessThanEqualExpr.js +2 -1
  49. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileLessThanExpr.js +2 -1
  50. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileMultExpr.js +24 -6
  51. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileNotEqualExpr.js +2 -1
  52. package/dist/compiler/AstCompiler/internal/exprs/binary/_compileSubExpr.js +16 -5
  53. package/dist/compiler/AstCompiler/internal/statements/_compileMatchStmt.js +33 -20
  54. package/dist/compiler/AstCompiler/utils/getPropAccessReturnType.js +11 -0
  55. package/dist/compiler/Compiler.js +20 -27
  56. package/dist/compiler/TirCompiler/expressify/ExpressifyCtx.js +1 -1
  57. package/dist/compiler/TirCompiler/expressify/expressify.js +30 -2
  58. package/dist/compiler/TirCompiler/expressify/expressifyForStmt.d.ts +2 -1
  59. package/dist/compiler/TirCompiler/expressify/expressifyForStmt.js +45 -7
  60. package/dist/compiler/TirCompiler/expressify/expressifyVars.d.ts +0 -1
  61. package/dist/compiler/TirCompiler/expressify/expressifyVars.js +23 -8
  62. package/dist/compiler/tir/expressions/TirCaseExpr.d.ts +9 -0
  63. package/dist/compiler/tir/expressions/TirCaseExpr.js +144 -122
  64. package/dist/compiler/tir/expressions/TirElemAccessExpr.js +2 -2
  65. package/dist/compiler/tir/expressions/TirFromDataExpr.js +102 -67
  66. package/dist/compiler/tir/expressions/TirIsExpr.js +14 -1
  67. package/dist/compiler/tir/expressions/TirNativeFunc.d.ts +1 -2
  68. package/dist/compiler/tir/expressions/TirNativeFunc.js +2 -12
  69. package/dist/compiler/tir/expressions/TirToDataExpr.js +3 -0
  70. package/dist/compiler/tir/expressions/TirTypeConversionExpr.js +10 -0
  71. package/dist/compiler/tir/expressions/TirVariableAccessExpr.d.ts +2 -3
  72. package/dist/compiler/tir/expressions/TirVariableAccessExpr.js +1 -4
  73. package/dist/compiler/tir/expressions/ToIRTermCtx.d.ts +20 -3
  74. package/dist/compiler/tir/expressions/ToIRTermCtx.js +48 -3
  75. package/dist/compiler/tir/expressions/binary/TirBinaryExpr.d.ts +2 -2
  76. package/dist/compiler/tir/expressions/binary/TirBinaryExpr.js +45 -8
  77. package/dist/compiler/tir/expressions/litteral/TirLitEnumMemberExpr.d.ts +19 -0
  78. package/dist/compiler/tir/expressions/litteral/TirLitEnumMemberExpr.js +24 -0
  79. package/dist/compiler/tir/expressions/litteral/TirLitteralExpr.d.ts +2 -1
  80. package/dist/compiler/tir/expressions/litteral/TirLitteralExpr.js +2 -0
  81. package/dist/compiler/tir/expressions/unary/TirUnaryMinus.js +4 -1
  82. package/dist/compiler/tir/program/stdScope/populateStdNamespace.js +49 -4
  83. package/dist/compiler/tir/program/stdScope/prelude/preludeTypesSrc.js +35 -2
  84. package/dist/compiler/tir/program/stdScope/stdScope.d.ts +7 -0
  85. package/dist/compiler/tir/program/stdScope/stdScope.js +83 -40
  86. package/dist/compiler/tir/types/TirEnumType.d.ts +21 -0
  87. package/dist/compiler/tir/types/TirEnumType.js +36 -0
  88. package/dist/compiler/tir/types/TirNativeType/TirNativeType.d.ts +4 -2
  89. package/dist/compiler/tir/types/TirNativeType/TirNativeType.js +5 -0
  90. package/dist/compiler/tir/types/TirNativeType/native/array.d.ts +16 -0
  91. package/dist/compiler/tir/types/TirNativeType/native/array.js +38 -0
  92. package/dist/compiler/tir/types/TirNativeType/native/index.d.ts +2 -0
  93. package/dist/compiler/tir/types/TirNativeType/native/index.js +2 -0
  94. package/dist/compiler/tir/types/TirNativeType/native/value.d.ts +18 -0
  95. package/dist/compiler/tir/types/TirNativeType/native/value.js +17 -0
  96. package/dist/compiler/tir/types/TirStructType.js +6 -1
  97. package/dist/compiler/tir/types/TirType.d.ts +3 -2
  98. package/dist/compiler/tir/types/TirType.js +4 -1
  99. package/dist/compiler/tir/types/utils/canAssignTo.js +28 -0
  100. package/dist/compiler/tir/types/utils/canCastTo.js +14 -1
  101. package/dist/compiler/tir/types/utils/getDeconstructableType.d.ts +2 -1
  102. package/dist/compiler/tir/types/utils/getDeconstructableType.js +2 -0
  103. package/dist/compiler/tir/types/utils/inferTypeArgs.js +4 -0
  104. package/dist/compiler/tir/types/utils/normalizeEnumToInt.d.ts +10 -0
  105. package/dist/compiler/tir/types/utils/normalizeEnumToInt.js +17 -0
  106. package/dist/compiler/tir/types/utils/substituteTypeParams.js +5 -0
  107. package/dist/diagnostics/diagnosticMessages.generated.d.ts +5 -0
  108. package/dist/diagnostics/diagnosticMessages.generated.js +10 -0
  109. package/dist/parser/Parser.js +29 -13
  110. package/dist/tokenizer/Token.d.ts +8 -7
  111. package/dist/tokenizer/Token.js +8 -7
  112. package/dist/tokenizer/utils/tokenFromKeyword.js +2 -0
  113. package/dist/version.generated.d.ts +1 -1
  114. package/dist/version.generated.js +1 -1
  115. package/package.json +3 -3
  116. package/dist/IR/tree_utils/_ir_lazyChooseList.d.ts +0 -3
  117. package/dist/IR/tree_utils/_ir_lazyChooseList.js +0 -7
@@ -84,6 +84,18 @@ export interface CompilerOptions {
84
84
  *
85
85
  **/
86
86
  addMarker: boolean;
87
+ /**
88
+ * Controls the default Data encoding for single-constructor shortcut
89
+ * struct declarations (`struct Foo { x: int }`).
90
+ *
91
+ * - `"default"`: shortcut single-constructor structs are encoded as
92
+ * `constrData(0, fields)` (backwards-compatible). Use the explicit
93
+ * `untagged` keyword to opt a specific struct in to the minimal form.
94
+ * - `"minimal"`: shortcut single-constructor structs are encoded as
95
+ * `listData(fields)` (smaller / cheaper). The explicit `untagged`
96
+ * keyword still forces minimal regardless.
97
+ */
98
+ encodingStrategy: "default" | "minimal";
87
99
  }
88
100
  /**
89
101
  * Option templates intentionally omit `compilerVersion` — every caller must
@@ -1,6 +1,7 @@
1
1
  import { isObject } from "@harmoniclabs/obj-utils";
2
- import { defaultUplcVersion, UPLCVersion } from "@harmoniclabs/uplc";
2
+ import { UPLCVersion } from "@harmoniclabs/uplc";
3
3
  import { COMPILER_VERSION } from "../../version.generated.js";
4
+ const defaultTargetUplcVersion = new UPLCVersion(1, 1, 0);
4
5
  export { COMPILER_VERSION };
5
6
  export const productionUplcOptimizations = Object.freeze({
6
7
  groupApplications: true,
@@ -39,33 +40,36 @@ export const extremeOptions = Object.freeze({
39
40
  root: ".",
40
41
  outDir: "./out",
41
42
  silent: false,
42
- targetUplcVersion: defaultUplcVersion,
43
+ targetUplcVersion: defaultTargetUplcVersion,
43
44
  removeTraces: true,
44
45
  delayHoists: true,
45
46
  uplcOptimizations: productionUplcOptimizations,
46
- addMarker: true
47
+ addMarker: true,
48
+ encodingStrategy: "minimal"
47
49
  });
48
50
  export const productionOptions = Object.freeze({
49
51
  entry: "./src/index.pebble",
50
52
  root: ".",
51
53
  outDir: "./out",
52
54
  silent: false,
53
- targetUplcVersion: defaultUplcVersion,
55
+ targetUplcVersion: defaultTargetUplcVersion,
54
56
  removeTraces: true,
55
57
  delayHoists: true,
56
58
  uplcOptimizations: productionUplcOptimizations,
57
- addMarker: true
59
+ addMarker: true,
60
+ encodingStrategy: "default"
58
61
  });
59
62
  export const debugOptions = Object.freeze({
60
63
  entry: "./src/index.pebble",
61
64
  root: ".",
62
65
  outDir: "./out",
63
66
  silent: false,
64
- targetUplcVersion: defaultUplcVersion,
67
+ targetUplcVersion: defaultTargetUplcVersion,
65
68
  removeTraces: false,
66
69
  delayHoists: false,
67
70
  uplcOptimizations: debugUplcOptimizations,
68
- addMarker: false
71
+ addMarker: false,
72
+ encodingStrategy: "default"
69
73
  });
70
74
  export const defaultOptions = Object.freeze({
71
75
  ...productionOptions,
@@ -77,7 +81,7 @@ export const testOptions = Object.freeze({
77
81
  });
78
82
  export const defulatCompilerOptions = defaultOptions;
79
83
  export function completeCompilerOptions(options, complete = defaultOptions) {
80
- let targetUplcVersion = options.targetUplcVersion instanceof UPLCVersion ? complete.targetUplcVersion : defaultUplcVersion;
84
+ let targetUplcVersion = options.targetUplcVersion instanceof UPLCVersion ? complete.targetUplcVersion : defaultTargetUplcVersion;
81
85
  complete = {
82
86
  ...defaultOptions,
83
87
  ...complete
@@ -107,6 +111,7 @@ export function completeCompilerOptions(options, complete = defaultOptions) {
107
111
  removeTraces: options.removeTraces ?? complete.removeTraces,
108
112
  delayHoists: options.delayHoists ?? complete.delayHoists,
109
113
  uplcOptimizations: completeUplcOptimizations(uplcOptimizations),
110
- addMarker: options.addMarker ?? complete.addMarker
114
+ addMarker: options.addMarker ?? complete.addMarker,
115
+ encodingStrategy: options.encodingStrategy ?? complete.encodingStrategy ?? "default"
111
116
  };
112
117
  }
@@ -16,6 +16,10 @@ import { IRCase, IRConstr, IRFunc, IRNative } from "../IRNodes/index.js";
16
16
  import { replaceForcedNativesWithHoisted } from "./subRoutines/replaceForcedNativesWithHoisted.js";
17
17
  import { performUplcOptimizationsAndReturnRoot } from "./subRoutines/performUplcOptimizationsAndReturnRoot/performUplcOptimizationsAndReturnRoot.js";
18
18
  import { rewriteNativesAppliedToConstantsAndReturnRoot } from "./subRoutines/rewriteNativesAppliedToConstantsAndReturnRoot.js";
19
+ import { rewriteToCaseOverConstAndReturnRoot } from "./subRoutines/rewriteToCaseOverConstAndReturnRoot.js";
20
+ import { rewriteHeadTailInCaseConsAndReturnRoot } from "./subRoutines/rewriteHeadTailInCaseConsAndReturnRoot.js";
21
+ import { introduceCaseForDualHeadTailAndReturnRoot } from "./subRoutines/introduceCaseForDualHeadTailAndReturnRoot.js";
22
+ import { inlineSingleUseLetBindingsAndReturnRoot } from "./subRoutines/inlineSingleUseLetBindingsAndReturnRoot.js";
19
23
  import { _debug_assertClosedIR } from "../utils/index.js";
20
24
  import { ToUplcCtx } from "./ctx/ToUplcCtx.js";
21
25
  import { removeUnusedVarsAndReturnRoot } from "./subRoutines/removeUnusuedVarsAndReturnRoot/removeUnusuedVarsAndReturnRoot.js";
@@ -57,6 +61,29 @@ export function compileIRToUPLC(term, paritalOptions = defaultOptions) {
57
61
  term = replaceNativesAndReturnRoot(term);
58
62
  // re-call rewrite to optimize introduced hoisted
59
63
  term = rewriteNativesAppliedToConstantsAndReturnRoot(term);
64
+ // Lower `strictIfThenElse` triple-apps to `IRCase` BEFORE
65
+ // `replaceForcedNativesWithHoisted` would otherwise hoist
66
+ // `(force ifThenElse)` into a shared variable that's no longer
67
+ // pattern-matchable as a native. (`strictChooseList` is already
68
+ // lowered to `IRCase` unconditionally by the earlier
69
+ // `rewriteNativesAppliedToConstantsAndReturnRoot` pass.)
70
+ term = rewriteToCaseOverConstAndReturnRoot(term);
71
+ // Inside `case L of cons h t -> body`, replace any `headList(L)` /
72
+ // `tailList(L)` calls within `body` with `h` / `t`. Drop the now-dead
73
+ // `h`/`t` bindings via a fresh unused-vars sweep.
74
+ term = rewriteHeadTailInCaseConsAndReturnRoot(term);
75
+ term = removeUnusedVarsAndReturnRoot(term);
76
+ // For every IRFunc body where the same list L is accessed via BOTH
77
+ // `headList(L)` and `tailList(L)`, wrap the body in
78
+ // `case L of cons h t -> body' | nil -> error` and substitute the two
79
+ // builtin calls with `h` / `t`. Empirically (bench.headTailVsCase):
80
+ // one case dispatch costs ~128K CPU vs ~160K for two builtin calls,
81
+ // and only ~9 bytes vs ~16. The previous head/tail-in-case-cons pass
82
+ // can then make a second sweep to substitute any further internal
83
+ // references the new case introduced.
84
+ term = introduceCaseForDualHeadTailAndReturnRoot(term);
85
+ term = rewriteHeadTailInCaseConsAndReturnRoot(term);
86
+ term = removeUnusedVarsAndReturnRoot(term);
60
87
  // debugAsserts && _debug_assertions( term );
61
88
  // unwrap top level letted and hoisted;
62
89
  // some natives may be converted to hoisted;
@@ -144,12 +171,21 @@ export function compileIRToUPLC(term, paritalOptions = defaultOptions) {
144
171
  // somethingWasInlined = inlineResult.somethingWasInlined;
145
172
  // }
146
173
  term = removeUnusedVarsAndReturnRoot(term);
174
+ // After `handleLettedAndReturnRoot` lowers `IRLetted` into the
175
+ // `IRApp(IRFunc([p], body), value)` shape, this is the first point
176
+ // where the let-as-application pattern is syntactically visible.
177
+ // Run the inliner here (NOT earlier, where lets are still IRLetted
178
+ // nodes the inliner doesn't recognize). Single-use uses trapped
179
+ // inside nested closures are skipped — see the pass for details.
180
+ term = inlineSingleUseLetBindingsAndReturnRoot(term);
181
+ term = removeUnusedVarsAndReturnRoot(term);
147
182
  term = performUplcOptimizationsAndReturnRoot(term, options);
183
+ // Rewrite strictIfThenElse into IRCase-over-Const, and prune
184
+ // trailing IRError continuations from any IRCase.
185
+ term = rewriteToCaseOverConstAndReturnRoot(term);
148
186
  term = ensureProperlyForcedBuiltinsAndReturnRoot(term);
149
187
  if (options.addMarker &&
150
- options.targetUplcVersion.major >= 1 &&
151
- options.targetUplcVersion.minor >= 1 &&
152
- options.targetUplcVersion.patch >= 0) {
188
+ options.targetUplcVersion.isV3Friendly()) {
153
189
  term = new IRCase(new IRConstr(0, []), [
154
190
  term,
155
191
  // never evaluated
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Inline single-use let-bindings of the shape `((λp. body) value)`:
3
+ *
4
+ * - If `body` references `p` exactly once AND that reference is NOT
5
+ * trapped inside a nested closure (IRFunc/IRRecursive) within
6
+ * `body`, replace the entire app with `body[p := value]`. The
7
+ * nested-closure guard is critical: a use that's syntactically
8
+ * "once" inside a recursive body is actually evaluated once per
9
+ * iteration, and inlining there would move per-iteration work to a
10
+ * value the caller currently evaluates once.
11
+ * - If `body` references `p` zero times, drop the let and replace
12
+ * the app with `body`.
13
+ *
14
+ * IRCase continuations are themselves IRFunc nodes (case-cons branches
15
+ * take h/t lambdas), but the case dispatches each continuation AT MOST
16
+ * ONCE per case eval, so they don't count as "trapping" closures — we
17
+ * treat IRFunc reached *as a direct case continuation* as transparent.
18
+ *
19
+ * If `body` uses `p` two or more times we don't inline either (would
20
+ * duplicate the term in the output).
21
+ */
22
+ import { IRTerm } from "../../IRTerm.js";
23
+ export declare function inlineSingleUseLetBindingsAndReturnRoot(term: IRTerm): IRTerm;
@@ -0,0 +1,263 @@
1
+ /**
2
+ * Inline single-use let-bindings of the shape `((λp. body) value)`:
3
+ *
4
+ * - If `body` references `p` exactly once AND that reference is NOT
5
+ * trapped inside a nested closure (IRFunc/IRRecursive) within
6
+ * `body`, replace the entire app with `body[p := value]`. The
7
+ * nested-closure guard is critical: a use that's syntactically
8
+ * "once" inside a recursive body is actually evaluated once per
9
+ * iteration, and inlining there would move per-iteration work to a
10
+ * value the caller currently evaluates once.
11
+ * - If `body` references `p` zero times, drop the let and replace
12
+ * the app with `body`.
13
+ *
14
+ * IRCase continuations are themselves IRFunc nodes (case-cons branches
15
+ * take h/t lambdas), but the case dispatches each continuation AT MOST
16
+ * ONCE per case eval, so they don't count as "trapping" closures — we
17
+ * treat IRFunc reached *as a direct case continuation* as transparent.
18
+ *
19
+ * If `body` uses `p` two or more times we don't inline either (would
20
+ * duplicate the term in the output).
21
+ */
22
+ import { IRApp } from "../../IRNodes/IRApp.js";
23
+ import { IRCase } from "../../IRNodes/IRCase.js";
24
+ import { IRConst } from "../../IRNodes/IRConst.js";
25
+ import { IRDelayed } from "../../IRNodes/IRDelayed.js";
26
+ import { IRFunc } from "../../IRNodes/IRFunc.js";
27
+ import { IRHoisted } from "../../IRNodes/IRHoisted.js";
28
+ import { IRLetted } from "../../IRNodes/IRLetted.js";
29
+ import { IRRecursive } from "../../IRNodes/IRRecursive.js";
30
+ import { IRVar } from "../../IRNodes/IRVar.js";
31
+ import { _modifyChildFromTo } from "../_internal/_modifyChildFromTo.js";
32
+ export function inlineSingleUseLetBindingsAndReturnRoot(term) {
33
+ // Iterate until a full pass finds nothing to rewrite. Each rewrite
34
+ // restarts the walk from the (possibly new) root because the tree
35
+ // shape near the rewrite changes and may expose further candidates.
36
+ let didChange = true;
37
+ while (didChange) {
38
+ didChange = false;
39
+ const stack = [term];
40
+ outer: while (stack.length > 0) {
41
+ const t = stack.pop();
42
+ if (t instanceof IRApp
43
+ && t.fn instanceof IRFunc
44
+ && t.fn.params.length === 1) {
45
+ const fn = t.fn;
46
+ const p = fn.params[0];
47
+ const value = t.arg;
48
+ // Beta-reduce when the argument is itself just a
49
+ // variable access. Replacing every `IRVar(p)` with
50
+ // `IRVar(x)` costs the same per access (a single env
51
+ // lookup either way) and saves the surrounding
52
+ // `(λp. …) x` lambda + application. Always safe,
53
+ // regardless of how many times `p` is used and
54
+ // regardless of whether the uses sit inside a recursive
55
+ // body — there's no work to multiply.
56
+ if (value instanceof IRVar) {
57
+ const newBody = substituteAllVar(fn.body, p, value);
58
+ term = replaceWithBody(term, t, newBody);
59
+ didChange = true;
60
+ break outer;
61
+ }
62
+ const stats = countVarUses(fn.body, p);
63
+ if (stats.count === 0) {
64
+ // Dead let — replace with body, dropping the value.
65
+ term = replaceWithBody(term, t, fn.body);
66
+ didChange = true;
67
+ break outer;
68
+ }
69
+ if (stats.count === 1 && !stats.trapped) {
70
+ substituteVar(fn.body, p, value);
71
+ term = replaceWithBody(term, t, fn.body);
72
+ didChange = true;
73
+ break outer;
74
+ }
75
+ // Trapped single use: the lone occurrence sits inside a
76
+ // nested closure (recursive body or non-case lambda).
77
+ // Inlining a COMPUTATION there would duplicate the
78
+ // per-iteration work, but a syntactic VALUE (closure /
79
+ // constant / var / delay) only pays its (essentially
80
+ // zero) construction cost — the same as a fresh lookup.
81
+ // Pebble also stores recursive helpers as open-recursion
82
+ // lambdas whose body references their own binding name;
83
+ // inlining such a lambda would orphan that self-ref, so
84
+ // also require that the value has no free `IRVar(p)`.
85
+ if (stats.count === 1
86
+ && stats.trapped
87
+ && isSyntacticValue(value)
88
+ && !containsFreeVar(value, p)) {
89
+ substituteVar(fn.body, p, value);
90
+ term = replaceWithBody(term, t, fn.body);
91
+ didChange = true;
92
+ break outer;
93
+ }
94
+ // count >= 2, or trapped-with-computational-value → leave alone
95
+ }
96
+ stack.push(...t.children());
97
+ }
98
+ }
99
+ return term;
100
+ }
101
+ /** Returns true if `term` contains a free `IRVar(p)` reference. A
102
+ * binding inside `term` that re-uses `p`'s symbol shadows it (these
103
+ * are rare given fresh-symbol minting, but the check is cheap). */
104
+ function containsFreeVar(term, p) {
105
+ const stack = [term];
106
+ while (stack.length > 0) {
107
+ const t = stack.pop();
108
+ if (t instanceof IRVar && t.name === p)
109
+ return true;
110
+ if (t instanceof IRFunc && t.params.includes(p))
111
+ continue;
112
+ if (t instanceof IRRecursive && t.name === p)
113
+ continue;
114
+ stack.push(...t.children());
115
+ }
116
+ return false;
117
+ }
118
+ /**
119
+ * A "syntactic value" is a term whose evaluation does no work beyond
120
+ * binding/closure construction — duplicating it across a recursive
121
+ * boundary doesn't multiply runtime cost. Specifically:
122
+ *
123
+ * - `IRFunc` / `IRRecursive` — closure values; per-construction is
124
+ * ~zero cost, and the body only runs when the closure is applied
125
+ * (which happens at the use-site frequency either way).
126
+ * - `IRConst` — literal value.
127
+ * - `IRVar` — already just an environment lookup.
128
+ * - `IRDelayed` — produces a thunk; the inner term only runs on
129
+ * force, at the use-site frequency.
130
+ *
131
+ * NOTE: `IRNative` is deliberately NOT included. While a bare native
132
+ * reference is itself cheap, in Pebble's pipeline `hoistForcedNatives`
133
+ * specifically wraps each forced builtin (e.g. `headList`,
134
+ * `tailList`, `ifThenElse`) once at the top via `(λvar. …) IRNative`,
135
+ * so that the runtime `force` happens once and is shared. Inlining
136
+ * the `IRNative` back into a recursive body undoes that sharing —
137
+ * the resulting compiled UPLC re-issues the force per iteration.
138
+ * `IRHoisted`/`IRLetted` are transparent wrappers: we unwrap to check
139
+ * the inner term.
140
+ */
141
+ function isSyntacticValue(t) {
142
+ while (t instanceof IRHoisted)
143
+ t = t.hoisted;
144
+ while (t instanceof IRLetted)
145
+ t = t.value;
146
+ return (t instanceof IRFunc
147
+ || t instanceof IRRecursive
148
+ || t instanceof IRConst
149
+ || t instanceof IRVar
150
+ || t instanceof IRDelayed);
151
+ }
152
+ function replaceWithBody(root, app, body) {
153
+ const parent = app.parent;
154
+ if (parent === undefined) {
155
+ body.parent = undefined;
156
+ return body;
157
+ }
158
+ _modifyChildFromTo(parent, app, body);
159
+ return root;
160
+ }
161
+ /** Count IRVar(p) references in `body` accurately. Caps at 2 — we
162
+ * only care about 0/1/many — but ALWAYS walks the whole tree, so we
163
+ * correctly distinguish "1 trapped use" from "1 trapped + 1 more".
164
+ */
165
+ function countVarUses(body, p) {
166
+ let count = 0;
167
+ let firstUseTrapped = false;
168
+ const stack = [{ node: body, trapped: false }];
169
+ while (stack.length > 0) {
170
+ const { node: t, trapped } = stack.pop();
171
+ if (t instanceof IRVar) {
172
+ if (t.name === p) {
173
+ if (count === 0)
174
+ firstUseTrapped = trapped;
175
+ count++;
176
+ if (count >= 2)
177
+ return { count: 2, trapped: false };
178
+ }
179
+ continue;
180
+ }
181
+ // Symbols are minted fresh on each binder, so shadowing is rare,
182
+ // but the guard is cheap and protects against any reuse.
183
+ if (t instanceof IRFunc && t.params.includes(p))
184
+ continue;
185
+ if (t instanceof IRRecursive && t.name === p)
186
+ continue;
187
+ // Determine the trap state for descending into this node:
188
+ // - IRRecursive always traps (loop introduces multi-eval).
189
+ // - IRFunc traps EXCEPT when it's a direct case continuation
190
+ // (`IRCase(_, […, this IRFunc, …])`): the case dispatches
191
+ // it at most once per case eval, so it's transparent.
192
+ let childTrapped = trapped;
193
+ if (t instanceof IRRecursive)
194
+ childTrapped = true;
195
+ else if (t instanceof IRFunc) {
196
+ const parent = t.parent;
197
+ const isCaseContinuation = parent instanceof IRCase
198
+ // direct continuation array membership
199
+ && Array.from(parent.continuations).includes(t);
200
+ if (!isCaseContinuation)
201
+ childTrapped = true;
202
+ }
203
+ for (const c of t.children())
204
+ stack.push({ node: c, trapped: childTrapped });
205
+ }
206
+ return { count: count, trapped: firstUseTrapped };
207
+ }
208
+ /**
209
+ * Replace EVERY `IRVar(p)` inside `body` with a fresh clone of
210
+ * `replacement`. Used for the var-arg beta-reduction: substituting one
211
+ * variable name for another anywhere it appears.
212
+ *
213
+ * Returns the (possibly new) body. If `body` itself is `IRVar(p)`,
214
+ * returns a fresh clone of `replacement` — the caller is responsible
215
+ * for wiring it into the parent.
216
+ */
217
+ function substituteAllVar(body, p, replacement) {
218
+ if (body instanceof IRVar && body.name === p) {
219
+ return replacement.clone();
220
+ }
221
+ const stack = [body];
222
+ while (stack.length > 0) {
223
+ const t = stack.pop();
224
+ // Don't descend into scopes that shadow p (unique-symbol
225
+ // invariant makes this rare, but the guard is cheap).
226
+ if (t instanceof IRFunc && t.params.includes(p))
227
+ continue;
228
+ if (t instanceof IRRecursive && t.name === p)
229
+ continue;
230
+ // Snapshot children before mutating (children() returns a fresh
231
+ // array, so the loop is stable across modifications).
232
+ const children = t.children();
233
+ for (const child of children) {
234
+ if (child instanceof IRVar && child.name === p) {
235
+ // `t` must be a parent term because we got `child` from
236
+ // `t.children()`. Cast to satisfy the type checker.
237
+ _modifyChildFromTo(t, child, replacement.clone());
238
+ }
239
+ else {
240
+ stack.push(child);
241
+ }
242
+ }
243
+ }
244
+ return body;
245
+ }
246
+ /** Replace the (single) IRVar(p) inside `body` with `replacement`. */
247
+ function substituteVar(body, p, replacement) {
248
+ const stack = [body];
249
+ while (stack.length > 0) {
250
+ const t = stack.pop();
251
+ if (t instanceof IRVar && t.name === p) {
252
+ const parent = t.parent;
253
+ if (parent !== undefined)
254
+ _modifyChildFromTo(parent, t, replacement);
255
+ return;
256
+ }
257
+ if (t instanceof IRFunc && t.params.includes(p))
258
+ continue;
259
+ if (t instanceof IRRecursive && t.name === p)
260
+ continue;
261
+ stack.push(...t.children());
262
+ }
263
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * When the same list `L` is used for BOTH `headList(L)` and `tailList(L)`
3
+ * within a single function body, those two builtin calls can be replaced
4
+ * by a single `case L of cons h t -> body[h/headList(L), t/tailList(L)] |
5
+ * nil -> error`. The trade-off (measured in `bench.headTailVsCase`):
6
+ *
7
+ * - one `headList` (or `tailList`) builtin call: 112,100 CPU / 800 mem
8
+ * - one `case L of cons h t -> h | nil -> error`: 128,100 CPU / 900 mem
9
+ *
10
+ * So for a single access the builtin wins; for dual access the case wins
11
+ * by a single dispatch (~96K CPU / 700 mem).
12
+ *
13
+ * This pass walks every `IRFunc` body bottom-up. For each body, it scans
14
+ * the top-level (without descending into nested `IRFunc`/`IRRecursive`,
15
+ * because nested scopes are processed independently). If a free list var
16
+ * `L` has both a `headList(L)` and a `tailList(L)` use in the body, the
17
+ * body is wrapped with `IRCase(IRVar(L), [IRFunc([h, t], body'), IRError])`
18
+ * — where `body'` has those calls replaced with `IRVar(h)` / `IRVar(t)`.
19
+ *
20
+ * Notes:
21
+ * - We only act on `IRFunc` bodies that are NOT immediate continuations
22
+ * of an `IRCase` whose scrutinee is `IRVar(L)` for the same L — that
23
+ * case-cons branch already binds head/tail and the prior
24
+ * `rewriteHeadTailInCaseConsAndReturnRoot` pass has already done the
25
+ * substitution. Wrapping again would be a no-op constructor pair.
26
+ * - The nil branch is `IRError`. Original code that calls `headList` or
27
+ * `tailList` on a nil list errors at evaluation; the new code errors
28
+ * at the case dispatch — same observable behavior whenever either
29
+ * call is actually reached.
30
+ * - The pass iterates: after introducing a case for L, the (now
31
+ * substituted) body might still contain a different L' with dual
32
+ * head/tail uses — handled by re-scanning until no more pairs.
33
+ */
34
+ import { IRTerm } from "../../IRTerm.js";
35
+ export declare function introduceCaseForDualHeadTailAndReturnRoot(term: IRTerm): IRTerm;
@@ -0,0 +1,169 @@
1
+ /**
2
+ * When the same list `L` is used for BOTH `headList(L)` and `tailList(L)`
3
+ * within a single function body, those two builtin calls can be replaced
4
+ * by a single `case L of cons h t -> body[h/headList(L), t/tailList(L)] |
5
+ * nil -> error`. The trade-off (measured in `bench.headTailVsCase`):
6
+ *
7
+ * - one `headList` (or `tailList`) builtin call: 112,100 CPU / 800 mem
8
+ * - one `case L of cons h t -> h | nil -> error`: 128,100 CPU / 900 mem
9
+ *
10
+ * So for a single access the builtin wins; for dual access the case wins
11
+ * by a single dispatch (~96K CPU / 700 mem).
12
+ *
13
+ * This pass walks every `IRFunc` body bottom-up. For each body, it scans
14
+ * the top-level (without descending into nested `IRFunc`/`IRRecursive`,
15
+ * because nested scopes are processed independently). If a free list var
16
+ * `L` has both a `headList(L)` and a `tailList(L)` use in the body, the
17
+ * body is wrapped with `IRCase(IRVar(L), [IRFunc([h, t], body'), IRError])`
18
+ * — where `body'` has those calls replaced with `IRVar(h)` / `IRVar(t)`.
19
+ *
20
+ * Notes:
21
+ * - We only act on `IRFunc` bodies that are NOT immediate continuations
22
+ * of an `IRCase` whose scrutinee is `IRVar(L)` for the same L — that
23
+ * case-cons branch already binds head/tail and the prior
24
+ * `rewriteHeadTailInCaseConsAndReturnRoot` pass has already done the
25
+ * substitution. Wrapping again would be a no-op constructor pair.
26
+ * - The nil branch is `IRError`. Original code that calls `headList` or
27
+ * `tailList` on a nil list errors at evaluation; the new code errors
28
+ * at the case dispatch — same observable behavior whenever either
29
+ * call is actually reached.
30
+ * - The pass iterates: after introducing a case for L, the (now
31
+ * substituted) body might still contain a different L' with dual
32
+ * head/tail uses — handled by re-scanning until no more pairs.
33
+ */
34
+ import { IRApp } from "../../IRNodes/IRApp.js";
35
+ import { IRCase } from "../../IRNodes/IRCase.js";
36
+ import { IRError } from "../../IRNodes/IRError.js";
37
+ import { IRFunc } from "../../IRNodes/IRFunc.js";
38
+ import { IRHoisted } from "../../IRNodes/IRHoisted.js";
39
+ import { IRLetted } from "../../IRNodes/IRLetted.js";
40
+ import { IRNative } from "../../IRNodes/IRNative/index.js";
41
+ import { IRNativeTag } from "../../IRNodes/IRNative/IRNativeTag.js";
42
+ import { IRRecursive } from "../../IRNodes/IRRecursive.js";
43
+ import { IRVar } from "../../IRNodes/IRVar.js";
44
+ import { _modifyChildFromTo } from "../_internal/_modifyChildFromTo.js";
45
+ function unwrap(t) {
46
+ while (t instanceof IRHoisted)
47
+ t = t.hoisted;
48
+ while (t instanceof IRLetted)
49
+ t = t.value;
50
+ return t;
51
+ }
52
+ export function introduceCaseForDualHeadTailAndReturnRoot(term) {
53
+ processNode(term);
54
+ return term;
55
+ }
56
+ function processNode(node) {
57
+ // Post-order: recurse into children first, then process this node.
58
+ for (const child of node.children())
59
+ processNode(child);
60
+ if (node instanceof IRFunc) {
61
+ // Avoid wrapping case-cons branches that the previous pass already
62
+ // optimized: if `node` is the cons continuation of an outer
63
+ // `IRCase(IRVar(L), [node, ...])`, then head(L)/tail(L) were
64
+ // already substituted; if any dual remains it's for a *different*
65
+ // list L', which we handle normally.
66
+ tryWrap(node);
67
+ }
68
+ }
69
+ function tryWrap(fn) {
70
+ while (true) {
71
+ const choice = findFirstDualHeadTailList(fn.body, new Set(fn.params));
72
+ if (choice === undefined)
73
+ return;
74
+ const listSym = choice;
75
+ const desc = listSym.description ?? "L";
76
+ const hSym = Symbol("h_" + desc);
77
+ const tSym = Symbol("t_" + desc);
78
+ // Substitute head(L)/tail(L) inside the current body (won't descend
79
+ // into nested scopes that re-bind L, h, or t — but we just minted
80
+ // h/t so they don't collide with anything).
81
+ substituteHeadTailInBody(fn.body, listSym, hSym, tSym);
82
+ // Wrap.
83
+ const oldBody = fn.body;
84
+ const consFn = new IRFunc([hSym, tSym], oldBody);
85
+ const wrapped = new IRCase(new IRVar(listSym), [consFn, new IRError()]);
86
+ fn.body = wrapped;
87
+ }
88
+ }
89
+ /**
90
+ * Scan `root` for free `headList(L)`/`tailList(L)` uses where `L` is NOT
91
+ * shadowed by an enclosing nested IRFunc/IRRecursive within `root`. Returns
92
+ * the first list symbol with at least one head AND one tail use.
93
+ */
94
+ function findFirstDualHeadTailList(root, boundInOuterFunc) {
95
+ const heads = new Set();
96
+ const tails = new Set();
97
+ function walk(node, locallyBound) {
98
+ if (node instanceof IRFunc) {
99
+ const next = new Set(locallyBound);
100
+ for (const p of node.params)
101
+ next.add(p);
102
+ walk(node.body, next);
103
+ return;
104
+ }
105
+ if (node instanceof IRRecursive) {
106
+ const next = new Set(locallyBound);
107
+ next.add(node.name);
108
+ walk(node.body, next);
109
+ return;
110
+ }
111
+ if (node instanceof IRApp) {
112
+ const fn = unwrap(node.fn);
113
+ const arg = node.arg;
114
+ if (fn instanceof IRNative
115
+ && arg instanceof IRVar
116
+ && !locallyBound.has(arg.name)
117
+ && (fn.tag === IRNativeTag.headList
118
+ || fn.tag === IRNativeTag.tailList)) {
119
+ (fn.tag === IRNativeTag.headList ? heads : tails).add(arg.name);
120
+ // do not descend further — this is a leaf for our purposes
121
+ return;
122
+ }
123
+ // generic descent
124
+ }
125
+ for (const c of node.children())
126
+ walk(c, locallyBound);
127
+ }
128
+ walk(root, boundInOuterFunc);
129
+ for (const sym of heads) {
130
+ if (tails.has(sym))
131
+ return sym;
132
+ }
133
+ return undefined;
134
+ }
135
+ /**
136
+ * In-place: replace every `IRApp(unwrap=headList, IRVar(L))` /
137
+ * `IRApp(unwrap=tailList, IRVar(L))` inside `root` with `IRVar(h)` /
138
+ * `IRVar(t)`. Stops descending into any nested IRFunc/IRRecursive that
139
+ * shadows `L`, `h`, or `t`.
140
+ */
141
+ function substituteHeadTailInBody(root, listSym, hSym, tSym) {
142
+ const stack = [root];
143
+ while (stack.length > 0) {
144
+ const t = stack.pop();
145
+ if (t instanceof IRFunc || t instanceof IRRecursive) {
146
+ const params = t instanceof IRFunc ? t.params : [t.name];
147
+ if (params.includes(listSym)
148
+ || params.includes(hSym)
149
+ || params.includes(tSym))
150
+ continue;
151
+ }
152
+ if (t instanceof IRApp) {
153
+ const fn = unwrap(t.fn);
154
+ const arg = t.arg;
155
+ if (fn instanceof IRNative
156
+ && arg instanceof IRVar
157
+ && arg.name === listSym
158
+ && (fn.tag === IRNativeTag.headList || fn.tag === IRNativeTag.tailList)) {
159
+ const newSym = fn.tag === IRNativeTag.headList ? hSym : tSym;
160
+ const parent = t.parent;
161
+ if (parent !== undefined) {
162
+ _modifyChildFromTo(parent, t, new IRVar(newSym));
163
+ }
164
+ continue;
165
+ }
166
+ }
167
+ stack.push(...t.children());
168
+ }
169
+ }
@@ -1,3 +1,2 @@
1
1
  import { IRTerm } from "../../IRTerm.js";
2
- export declare function __unsafe_clear_hoisted_cache(): void;
3
2
  export declare function replaceHoistedWithLetted(term: IRTerm): void;