{
 "title": "Hazel's Architecture Around #2651",
 "date": "2026-10-21",
 "kind": "architecture",
 "question": "How is Hazel built around hazelgrove/hazel#2651 (Statics: a module binding analyzes its definition once, not twice), and what does the PR change in that architecture? From Matthew Hammer and Alexander Bandukwala (Modules II's owner) reviewing #2651 together on 2026-10-21, aiming to merge it that day; Matthew had tested its behaviour and asked how Hazel is built around it.",
 "nothing_timed": "Nothing was timed for this report. Six module programs were run and analyzed with the command-line tool built at dev 962a7cb009 and at #2651's 29c08fd8fa ('runs' below); every count is counted from the code; the timings quoted are #2651's own (its description), hazelgrove/hazel#2609's finding 7, and hazelgrove/hazel#2717's (its description and the notes of the run that built it: the command-line tool's analyze times on nested module chains, two runs per depth, and its tests' own time bounds, timed with Sys.time at its head and in throwaway builds; 'measurements_quoted', 'follow_up').",
 "read": [
  {
   "what": "dev",
   "commit": "962a7cb0091cad9a115f82f92433c4d6499f7135",
   "note": "read 2026-10-21 after a fetch"
  },
  {
   "what": "#2651 head",
   "pr": "hazelgrove/hazel#2651",
   "commit": "29c08fd8fa6126a6507ab47f4e56e8613276bf0c"
  },
  {
   "what": "#2651 first commit (the read-back)",
   "commit": "e3013b036053fbef28211eef476cb8090ee2bbda"
  },
  {
   "what": "#2651 merge base with dev",
   "commit": "34f9605ddfda9aa9e629586b31d285f98221bcc3"
  },
  {
   "what": "Modules II part 1 head",
   "pr": "hazelgrove/hazel#2493",
   "commit": "c4bae23739049d13d9dc8eacdfe5be66f9ebee0b"
  },
  {
   "what": "Modules II part 2 head",
   "pr": "hazelgrove/hazel#2494",
   "commit": "be90bf4b217609cb3c0e22429715cdd436162cf6"
  },
  {
   "what": "Modules II part 3 head",
   "pr": "hazelgrove/hazel#2495",
   "commit": "111775a60cde2fb050ae28a652b44a7b999814e5"
  },
  {
   "what": "Modules II part 9 head",
   "pr": "hazelgrove/hazel#2510",
   "commit": "b02ce6dab5eb3e4e79cbcfc29f21227415423172"
  },
  {
   "what": "#2603 head",
   "pr": "hazelgrove/hazel#2603",
   "commit": "4aa493c05a8d5bc1733e7db685afd821da03bee8"
  },
  {
   "what": "#2469 head (Modular editors, DefStatics)",
   "pr": "hazelgrove/hazel#2469",
   "commit": "b5eaa3303149d42f4d0e30f715ba6df2685f4926"
  },
  {
   "what": "parent of cc620bbbdc, the old Elaborator",
   "commit": "a81e3188fd13665af430882c17c64f6033a53b54"
  },
  {
   "what": "#2651 commit pushed during the review: code and test comments only",
   "pr": "hazelgrove/hazel#2651",
   "commit": "fa3eef62ce54e1e17a5823a8f455e811097d532d"
  },
  {
   "what": "#2651 head since the review: one test's name",
   "pr": "hazelgrove/hazel#2651",
   "commit": "b5a3e1d90aff91f5d7b37dbf1387dd7e6a7ae733"
  },
  {
   "what": "#2717 head (the follow-up: a module binding is a non-recursive let), three commits on #2651's b5a3e1d90a",
   "pr": "hazelgrove/hazel#2717",
   "commit": "ea23dd493093dc7977c950b994f97a82ed870e20",
   "note": "its new test files read; its code not read for this report; described from its PR description and the notes of the run that built it"
  }
 ],
 "method": "Six read-only readers of the code at the commits above, one subsystem each (the edit-to-statics pipeline, the statics core, modules, elaboration to evaluation, the PR itself, its performance context); questions for Alexander drafted from two lenses and each checked for its premise and its need; a fact-check of every file:line at the commit its link names (gen024.py checks each linked range exists and writes its first line to drivers/refs-check.txt); figures rendered to PNG with ffmpeg's SVG reader in light and dark colours and looked at; not opened in a browser. Alexander's answers from the review and in conversation were relayed by Matthew and are paraphrased; his later answers on #2651 are paraphrased with a link to each comment. Afterwards the command-line tool was built at dev 962a7cb009 and at 29c08fd8fa and six module programs were run and analyzed with it ('runs'). GitHub (the PR's comments, reviews and checks) read 2026-10-21 19:34Z, and #2651's again at 20:34Z, with nothing new but #2717's cross-reference. The follow-up, hazelgrove/hazel#2717, is described from its description and from the notes of the run that built it, at ea23dd4930; its new test files were read, its code was not read for this report, and its tests were not run again here. The page was first generated by drivers/gen024.py and has been edited directly since, as the review went on. Finished 2026-10-21 14:41 MDT, once #2717 was open; later changes come as dated updates.",
 "headline": [
  {
   "value": "3 → 2 → 1",
   "what": "full analyses of a module literal's definition per module binding: dev, with #2651, with #2651 and #2603 (counted from the code)"
  },
  {
   "value": "1 more read-back",
   "what": "the ModuleExp case reads elab_term at the definition's id from the info map; statics already reads its own map back in three places"
  },
  {
   "value": "3^d → 2^d → 1",
   "what": "analyses of the innermost definition when module ... in bindings nest d deep in each other's definitions"
  },
  {
   "value": "M.x = 1",
   "what": "module M : { let x : Int } = { let x = 1; let y = 2 } in M.x: the module's raw source on dev, 1 with #2651, the same 3 static errors at both (run with the CLI); Alexander: dev's result is a bug"
  },
  {
   "value": "5 → stuck (run)",
   "what": "module M : { let x : Int } = 5 in M.x: 5 on dev, a stuck '5 .x' with #2651, no static errors either way (run with the CLI); Alexander accepts it (option 2): stuck and a static error is right, as Modules II part 1 already gives; its test goes on part 1"
  },
  {
   "value": "1 line",
   "what": "conflicts with Modules II parts 2 to 9 (merges cleanly with dev, part 1 and #2603); Alexander sees no conflict with Modules II; the resolution adds ~coercible=true to the fallback"
  },
  {
   "value": "5 answered (1 for now)",
   "what": "questions for Alexander, none holding up the merge; 3 design questions for Cyrus"
  }
 ],
 "measures": {
  "analyses": "full analyses (calls of uexp_to_info_map on the whole definition) of a module binding's definition, counting the let's recursion probe, whose map is thrown away; counted from the code, not measured"
 },
 "analysis_counts": {
  "columns": [
   "dev 962a7cb009",
   "#2651 29c08fd8fa",
   "#2603 alone 4aa493c05a",
   "#2651 + #2603"
  ],
  "rows": [
   {
    "definition": "module M = { ... } in body (a module literal)",
    "counts": [
     3,
     2,
     2,
     1
    ],
    "where": "probe Statics.re:2070, real pass 2079, direct pass 2762 (dev)"
   },
   {
    "definition": "module F = fun x -> x + 1 in F(2) (a function that does not name F)",
    "counts": [
     5,
     4,
     2,
     1
    ],
    "where": "probe 2070, def_base 2091 and def_base2 2103 (dropped), final 2121, direct 2762 (dev)"
   },
   {
    "definition": "module F = fun x -> F(x) in 1 (a function that names F)",
    "counts": [
     5,
     5,
     4,
     4
    ],
    "where": "as above; #2651 falls back (PR Statics.re:2781-2788); #2603 reuses the probe as def_base (4aa493c05a Statics.re:2135-2139)"
   }
  ]
 },
 "nesting": {
  "what": "analyses of the innermost definition when module ... in bindings sit in each other's definitions, depth d; a module item in braces is a plain let and is not counted here",
  "per_level": {
   "dev": 3,
   "#2651": 2,
   "#2603 alone": 2,
   "both": 1
  },
  "by_depth": {
   "1": [
    3,
    2,
    2,
    1
   ],
   "2": [
    9,
    4,
    4,
    1
   ],
   "3": [
    27,
    8,
    8,
    1
   ],
   "4": [
    81,
    16,
    16,
    1
   ]
  }
 },
 "measurements_quoted": [
  {
   "source": "hazelgrove/hazel#2651 description",
   "build": "dev with and without #2651",
   "what": "an 800-function module made up for timing, CLI, whole command",
   "runs": 2,
   "before_s": [
    6.06,
    6.74
   ],
   "after_s": [
    5.26,
    6.05
   ],
   "note": "overlapping: within run-to-run noise; statics about 0.13 s of 6 s with the fix"
  },
  {
   "source": "hazelgrove/hazel#2609 finding 7 and #2651's description",
   "build": "#2635 (livelits x modular editors), which also carries #2603's change",
   "what": "Shirt and Pants slide, profile",
   "share": "1.79 s of 4.2 s of statics in the second pass"
  },
  {
   "source": "hazelgrove/hazel#2609 finding 7",
   "build": "#2635",
   "what": "Shirt and Pants, CLI statics",
   "runs": 2,
   "before_s": 6.7,
   "after_s": 4.3,
   "baseline_s": 4.1,
   "baseline": "the same slide with the module flattened into top-level lets",
   "spread": "within 0.3 s"
  },
  {
   "source": "hazelgrove/hazel#2717 description",
   "build": "#2651 at b5a3e1d90a and #2717 at ea23dd4930",
   "what": "the tests' chains of nested modules, module F0 = fun x : Int -> module F1 = ... in F1(x) in F0(1), and the same with each definition a fix; depth n has n + 1 modules, F0 to Fn; wall time of the command-line tool's analyze, startup included",
   "runs": 2,
   "startup_s": "1.27 to 1.42 on both builds (analyzing the program 1, three runs per build)",
   "columns": [
    "functions, #2651",
    "functions, #2717",
    "fixes, #2651",
    "fixes, #2717"
   ],
   "by_depth_s": {
    "6": [
     [
      1.96,
      2.05
     ],
     [
      1.31,
      1.36
     ],
     [
      1.61,
      1.59
     ],
     [
      1.38,
      1.43
     ]
    ],
    "7": [
     [
      3.43,
      3.25
     ],
     [
      1.25,
      1.4
     ],
     [
      1.98,
      1.95
     ],
     [
      1.36,
      1.33
     ]
    ],
    "8": [
     [
      8.84,
      8.98
     ],
     [
      1.41,
      1.35
     ],
     [
      3.0,
      2.97
     ],
     [
      1.37,
      1.29
     ]
    ],
    "9": [
     [
      31.04,
      29.59
     ],
     [
      1.43,
      1.45
     ],
     [
      6.54,
      6.81
     ],
     [
      1.36,
      1.35
     ]
    ],
    "10": [
     "not run",
     [
      1.47,
      1.32
     ],
     [
      16.58,
      15.71
     ],
     [
      1.31,
      1.46
     ]
    ],
    "12": [
     "not run",
     [
      1.33,
      1.35
     ],
     "not run",
     [
      1.33,
      1.31
     ]
    ]
   },
   "note": "From depth 7 on, above startup, #2651's function chain grows about 3.8 to 3.9 times per level, which fits four analyses per module; its fix chain 2.6 to 3.3 times, which fits three. With #2717 both chains take 1.25 to 1.55 s at every depth measured, 4 to 12: within 0.15 s of startup's range, and not growing with depth. Two runs per point; the differences on #2651 are far above that spread, but this is not a benchmark."
  },
  {
   "source": "hazelgrove/hazel#2717 description and the notes of the run that built it",
   "build": "#2717 at ea23dd4930, and throwaway builds since reverted",
   "what": "the time-bound tests in Statics.Modules, timed by the tests with Sys.time",
   "rows": [
    {
     "test": "Twelve nested modules bound to fixpoints check in under 0.5 s",
     "#2717": "0.0 s",
     "#2651's Statics.re (b5a3e1d90a)": "197.2 s (23.1 s at ten), fails",
     "#2651 with names_itself skipping a fix": "1.7 s (the probe alone), fails"
    },
    {
     "test": "Nine nested function modules check in under 5 s",
     "#2717": "0.0 s",
     "the module's let made recursive again": "44.4 s, fails"
    },
    {
     "test": "Seven nested function modules at one-label types check in under 10 s",
     "#2717": "0.7 to 0.8 s",
     "the pre-pass not carrying the flag": "50.1 s, fails"
    }
   ]
  }
 ],
 "questions": [
  {
   "id": "q-one-member-signature-lift",
   "question": "Alexander, on dev should `module M : { let x : Int } = 5 in M.x` keep evaluating to 5? If so, #2651 must not read back the bare analysis whenever statics lifted the definition into a one-member signature: either it skips the read-back there, or autolabel_singleton_tuple first records what it returns. Either way it gives up the new result that its evaluator test pins for `module M : { let x : Int } = { let x = 1; let y = 2 } in M.x`.",
   "status": [
    "answered"
   ],
   "answer": [
    "Answered, Alexander, 2026-10-21: dev's result for the module with an extra member (its unelaborated source) is a bug, which comes from the auto-labelling of one-member tuples; that program should evaluate to 1, or be stuck. So #2651's 1 is a correct result: the behaviour change fixes a dev bug, and the PR's test pins the fix.",
    "Answered further, Alexander, on #2651, 2026-10-21. On the evaluator test (his comment): fine; the result matches how labeled tuples work. One could argue for making incompatible signatures stuck, but much of Hazel does not work that way, and his signature-sealing PR would make y private. On the bare value (his comment, answering Matthew's): option 2, accept it. A bare value under a module signature should be stuck and a static error; Modules II part 1 already gives both, with or without #2651, so on dev the missing error is temporary and #2651 needs no special case. Matthew's decision: the test for this program goes on part 1, not in #2651 (his reply)."
   ]
  },
  {
   "id": "q-merge-order-with-modules-ii",
   "question": "Alexander, can #2651 merge into dev ahead of your Modules II stack, with the one-line conflict in parts 2 to 9 resolved by keeping its read-back and adding `~coercible=true` to its fallback? That resolution is right only if, on your branches, the let's recorded analysis of the definition is the same analysis as the direct pass it replaces.",
   "status": [
    "answered"
   ],
   "answer": [
    "Answered, Alexander, 2026-10-21, in conversation, relayed by Matthew: he sees no conflict between #2651 and Modules II. He had not yet looked at #2603."
   ]
  },
  {
   "id": "q-are-module-bindings-recursive",
   "question": "Alexander, are module bindings meant never to be recursive, items inside `{ ... }` included, and if so, would you rather the module expansion tell the let that it is not recursive than have ModuleExp spot self-reference from the fixpoint in the let's elaboration, as #2651 now does?",
   "status": [
    "answered for now",
    "for Cyrus"
   ],
   "answer": [
    "Answered for now, Alexander, 2026-10-21: whether modules should be recursive is not settled. For now it is fine for them not to be recursive by default, since recursion has a number of known correctness problems; whether they should be is a question for Cyrus. On the signal: Modules II has none for this, and he did not write the original module keyword; as things stand a module binding is a let that is not recursive. His suggestion is an argument on the let analysis saying whether to check the recursive case, so that a module binding is never analyzed recursively.",
    "Answered further, Alexander, 2026-10-21. On the module keyword (in conversation, relayed by Matthew; his own agent's reading, which matches ours): it makes the binding non-recursive and allows a capitalized name, and is otherwise a let; #2651 keeps both. On #2651 he asked whether defining a module to be a function should be an error, addressing @disconcision on the self-naming test (his comment; For Cyrus). And on the self-naming signal (his comment): names_itself also matches a fixpoint the user wrote, module m = fix f -> …, which then keeps dev's results but loses the speed-up, and the two cannot easily be told apart by shape; so the signal belongs in the let analysis, as an argument that makes a module binding non-recursive, which can be a follow-up to keep #2651 small, and names_itself could go altogether if binding a module to a function becomes a static error (the guard). Matthew agreed (his reply), and the argument is now the draft follow-up #2717 (Statics: a module binding is a non-recursive let), opened that day at 20:31Z."
   ]
  },
  {
   "id": "q-def-elab-direct-on-modules-ii",
   "question": "Alexander, on your stack does ModuleExp still need its separate analysis and elaboration of the definition (def_elab_direct, which moduleexp_elab swaps in for the let's), or could a module binding use the let's elaboration as is, as your docs/modules.md already says it does?",
   "status": [
    "answered"
   ],
   "answer": [
    "Answered, Alexander, 2026-10-21, in conversation, relayed by Matthew, to the question as first asked (is the definition's elaboration before the let wraps it exactly what moduleexp_elab is meant to take?): yes, as he understands it, and it comes from the old elaborator. Reading the definition back from the stored info is closer to the original design than dev's second pass, because the old elaborator also read the definition from the statics map."
   ]
  },
  {
   "id": "q-tests-to-pin",
   "question": "Alexander, which of these module behaviours do you want pinned as tests in #2651 (a module binding analyzes its definition once): the self-naming error at its exact place, a function-valued module that does not name itself, and a module bound to a bare value under a one-member signature?",
   "status": [
    "answered"
   ],
   "answer": [
    "Answered on #2651, 2026-10-21. The third, the bare value: Alexander offered to pin it as stuck here, with a note that the static error comes with Modules II part 1, or to leave it out of #2651 and add it on part 1, where it can assert both the stuck term and the error (his comment); Matthew chose part 1 (his reply). The first, the self-naming error: Matthew answered that the let-analysis prototype pins the test to its mark and that its tests would include Alexander's two fix programs (Matthew's reply). The prototype is now the draft follow-up #2717 (Statics: a module binding is a non-recursive let), and it does both: the self-naming test and its form under a signature each pin one FreeConstructor mark at the inner F; the first fix program runs to 6 with no errors, and the second has one mark, Free(\"m\"), at the inner m. It also has the second, module F = fun x -> x + 1 in F(2), as an evaluator test (3)."
   ]
  }
 ],
 "for_cyrus": [
  {
   "id": "cyrus-recursive",
   "question": "Should module bindings be recursive?",
   "context": "Today module F = … as an expression is not (a self-reference is an error, and #2651 keeps it one), while a module F = … item inside { … } is lowered to a plain let and, by our reading, is. Alexander: for now it is fine for modules not to be recursive by default, since recursion has a number of known correctness problems (question 3)."
  },
  {
   "id": "cyrus-non-module",
   "question": "Should binding a module to something that is not a module be a static error?",
   "context": "module F = fun x -> 1 in F(3) is accepted today. With a signature the case is settled: module M : { let x : Int } = 5 in M.x, accepted on dev, is already a static error, and stuck, on Modules II part 1, which Alexander accepted as the intended behaviour, dev's missing error being temporary (question 1). If a module bound to a function becomes an error too, neither a self-naming function nor a module … = fix … can reach #2651's guard, and names_itself could go (the guard). Alexander asked a form of this on #2651 on 2026-10-21 at 18:25Z, in a review comment on its self-naming test (Test_Statics_Modules.re:411): \"should this be an error defining a module to be a function?\" (his comment). He addressed it to @disconcision, Andrew Blinn, who wrote the module keyword (b0735de6d6 to 131f508825 are by disconcision, the account Andrew opened #2469 (Modular editors) from), so it can be answered there, by Andrew or Cyrus."
  },
  {
   "id": "cyrus-keyword",
   "question": "What should the module keyword add over let?",
   "context": "Alexander had favoured plain let for module definitions; Cyrus wanted the keyword to do more. Today a module binding is checked as a let that is not recursive (Modules). By Alexander's reading (his agent's, in conversation, relayed by Matthew), which matches ours, the keyword makes the binding non-recursive and allows a capitalized name, and is otherwise a let; #2651 keeps both."
  }
 ],
 "files_note": "research-notes.json: the six readers' notes as given to the writer; questions.json: the five questions with their premise and need checks, as given, plus Alexander's answers (from the review, in conversation and on #2651) and each question's status; tables.json: every table in the page (tools/report_data.py).",
 "follow_up": {
  "pr": "hazelgrove/hazel#2717",
  "title": "Statics: a module binding is a non-recursive let",
  "state": "draft, opened 2026-10-21 20:31Z (14:31 MDT) against dev; CI at its head (https://github.com/hazelgrove/hazel/actions/runs/39671006602): Fast checks passed at 20:36Z, and Full tests were still running when this page was finished",
  "depends_on": "hazelgrove/hazel#2651: branch module-let-nonrec, three commits on #2651's head b5a3e1d90a; until #2651 merges, its diff against dev includes #2651's commits",
  "commits": [
   "a0a781effe A module binding is a non-recursive let, by an argument to the let",
   "e3dc50cc0b Tests that see which F a module definition binds, and what it costs",
   "ea23dd4930 Tests for a module bound to a fixpoint the user wrote"
  ],
  "what_it_does": "In Statics.re, uexp_to_info_map takes ~check_rec (true by default), which applies only to the term it is called on. With it false the let runs no recursion probe and has no recursive branch: it analyzes the definition once, in the outer context, where the bound name is not in scope, so the let itself reports a self-reference, and never wraps the definition in a FixF. ModuleExp passes ~check_rec=false to the let it expands to; the singleton-label pre-pass and autolabel_singleton_tuple's first analysis pass it along. names_itself and its fallback are gone; the definition's elaboration is still read back from the let's record.",
  "analyses_per_module_definition": {
   "how": "counted from the Let rule (its description)",
   "columns": [
    "#2651",
    "#2717"
   ],
   "rows": [
    {
     "definition": "a function that does not name the module",
     "counts": [
      4,
      1
     ]
    },
    {
     "definition": "a function that names the module (an error)",
     "counts": [
      5,
      1
     ]
    },
    {
     "definition": "a fix the user wrote",
     "counts": [
      3,
      1
     ]
    },
    {
     "definition": "anything else, such as { ... }",
     "counts": [
      2,
      1
     ]
    }
   ],
   "exception": "not new: a module binding checked against a one-label product type such as (a = Int) costs 3 (the ModuleExp's pre-pass, its let's pre-pass, the let's own pass); 6 for a function definition if the flag were not carried through the pre-pass"
  },
  "tests": {
   "at": "ea23dd4930",
   "passing": {
    "Statics.Modules": "86: 75 on #2651 plus 11 new; the self-naming test pins its one mark",
    "Evaluator.Modules": "24: 18 on #2651 plus 6 new",
    "Statics.Fixpoint": 1,
    "Evaluator.Fixpoints": 10,
    "Statics.Let": 2,
    "Evaluator.Let": 5,
    "Elaboration tests": 39,
    "Evaluator.Incremental": 41
   },
   "not_run": "the full suite, locally"
  },
  "fix_programs": [
   {
    "program": "module m = fix f -> fun n -> if n == 0 then 1 else n * f(n - 1) in m(3)",
    "result": "6, no static errors; its definition analyzed once (on #2651, three times)"
   },
   {
    "program": "module m = fix g -> fun n -> if n == 0 then 0 else m(n - 1) in m(3)",
    "result": "1 static error, 'Variable m is not bound' at the inner m (1:52), exactly one mark, Free(\"m\"); stops at the free m: m(2):Int",
    "on_2651": "the let's own analysis already reports m unbound: Exp.get_num_of_functions returns None for a FixF, so is_recursive is false and the let is not recursive; the fallback then reports the same mark again, at the same id. With names_itself made to skip a fix, the pinned Free(\"m\") test still passes; the skip keeps the recursion probe (two analyses per module)."
   }
  ],
  "statics_change": "module F = fun (x : Int) -> x > 0 in module F = fun x -> F(x) in F(1) + 1: no errors on #2651; on #2717 one error, 'Expecting type Int but got inconsistent type Bool' at F(1) (1:67), since the inner F is now the outer module. Both run it to true + 1, so the statics now agree with the evaluator.",
  "braces_items": "a module item inside { ... } is lowered to a plain Let (ModuleHelpers.wrap_item) and stays recursive, as on dev; tests pin the split (21 as a module ... in binding, 3 as an item); left for Alexander, Matthew and Cyrus to decide",
  "merge_notes": "recorded in e3dc50cc0b's message, parsed but not built: two Statics.re conflicts with Modules II part 2 (hazelgrove/hazel#2494 at be90bf4b21), one with hazelgrove/hazel#2603 (at 4aa493c05a)"
 },
 "status_as_of": "2026-10-21 14:41 MDT: every question for Alexander answered (question 3 for now); none holds up the merge. The let-analysis argument is the draft follow-up hazelgrove/hazel#2717 (Statics: a module binding is a non-recursive let), opened 20:31Z (14:31 MDT) against dev, on #2651's head b5a3e1d90a.",
 "runs": {
  "what": "module programs run and analyzed with the command-line tool (./hazel run and ./hazel analyze), built at dev and at #2651's head of the time; outputs as printed, not timings. 29c08fd8fa and the PR's later commits (fa3eef62ce, b5a3e1d90a) differ only in comments and a test's name.",
  "builds": {
   "dev": "962a7cb0091cad9a115f82f92433c4d6499f7135",
   "#2651": "29c08fd8fa6126a6507ab47f4e56e8613276bf0c"
  },
  "note": "The first row was found by reading the code (the check of question 1) and confirmed by this run; Matthew's comment on #2651 gave the same (https://github.com/hazelgrove/hazel/pull/2651#discussion_r4340596357).",
  "rows": [
   {
    "program": "module M : { let x : Int } = 5 in M.x",
    "dev": "5, no static errors",
    "pr_2651": "5 .x (stuck), no static errors",
    "path": "the one-member lift, with a bare value"
   },
   {
    "program": "module M : { let x : Int } = { let x = 1; let y = 2 } in M.x",
    "dev": "{ let x = 1; let y = 2 } (the raw source), 3 static errors (\"Expecting type Int but got inconsistent type x=Int, y=Int\")",
    "pr_2651": "1, the same 3 errors",
    "path": "the one-member lift, with an extra member"
   },
   {
    "program": "module F = fun x -> F(x) in 1",
    "dev": "runs to 1; 1 static error, \"Constructor is not defined\" at the inner F (1:21)",
    "pr_2651": "the same",
    "path": "the guard's fallback"
   },
   {
    "program": "module F = fun x -> x + 1 in F(2)",
    "dev": "3, no static errors",
    "pr_2651": "3, no static errors",
    "path": "the read-back after the let's recursive path, untested in the PR"
   },
   {
    "program": "module M : { let x : Int } = { let x = 42 } in M.x",
    "dev": "42, no static errors",
    "pr_2651": "42, no static errors",
    "path": "a module literal under a one-member signature, which on dev needs the swap of elaborations"
   },
   {
    "program": "module F : Int -> Int = fun x -> F(x) in 1",
    "dev": "runs to 1; \"Constructor is not defined\" at the inner F (1:34)",
    "pr_2651": "the same",
    "path": "the fallback's signature arm, Codecov's one missed line"
   }
  ]
 },
 "ci": {
  "fa3eef62ce": {
   "run": "https://github.com/hazelgrove/hazel/actions/runs/39655086864",
   "result": "Fast checks and Full tests passed, 18:59Z"
  },
  "b5a3e1d90a": {
   "run": "https://github.com/hazelgrove/hazel/actions/runs/39657741479",
   "result": "Fast checks passed 19:06Z, Full tests passed 19:26Z"
  },
  "read": "2026-10-21 19:34Z"
 }
}