Skip to content

Quick experiment: remove Ply dep#32

Draft
StachuDotNet wants to merge 42 commits intomainfrom
ply-to-task
Draft

Quick experiment: remove Ply dep#32
StachuDotNet wants to merge 42 commits intomainfrom
ply-to-task

Conversation

@StachuDotNet
Copy link
Copy Markdown
Owner

Wanted to see some numbers, ran this in the background. incomplete, may abandon or may revisit later.

Source kept on disk; restore when HTTP-server functionality returns.

- fsdark.sln: comment out BwdServer Project, ConfigurationPlatforms,
  and NestedProjects entries.
- Tests.fsproj: drop BwdServer ProjectReference; disable
  BwdServer.Tests.fs and HttpClient.Tests.fs (both reference
  BwdServer scaffolding).
- Tests.fs: inline BwdServer.Server.initSerializers (three
  Json.Vanilla.allow calls) so surviving Serialization.* tests still
  have the registrations they need; drop the BwdServer/HttpClient
  test list entries and init/Wait scaffolding.
Language is now the lowest project in the chain. Files keep their
existing F# module declarations (e.g. ``module LibExecution.RuntimeTypes``),
so namespace paths are unchanged and 18 downstream projects don't
need any edits — they still ProjectReference LibExecution and pick up
the new sub-projects transitively.

Layering (lowest to highest):

  Language      Prelude only
                  DarkDateTime · PackageRefs (shared primitives)
                  ProgramTypes · ProgramTypesAst · ProgramTypesParser
  Runtime       Prelude, Language
                  RuntimeTypes · ValueType · Blob · Stream
                  Dval · Builtin · TypeChecker · Interpreter · AnalysisTypes
                  RTQueryCompiler · ProgramTypesToRuntimeTypes
  DarkTypes     Prelude, Language, Runtime
                  DvalDecoder · CommonToDarkTypes
                  RuntimeTypesToDarkTypes · ProgramTypesToDarkTypes
  LibExecution  umbrella; just Execution.fs

The package-ref-hashes.txt embedded resource and EnsurePackageRefHashes
MSBuild target moved to Language alongside PackageRefs.fs; .gitignore
updated.

Build: 32 s on this machine, same as before — no new floor introduced.
The watcher's log redirected from _vscode-post-start-command via shell
``&>>`` had been growing without bound (15 MB / 86 K lines on a long
session). Now _build-server checks the file's size at startup and after
every compile cycle: above 5 MB, the file is rewritten in place to keep
only the most recent ~500 KB plus a header line noting the rotation.

In-place rewrite is required because the shell-redirected fd is held
open in append mode by the parent ``nohup`` process. Renaming the
file would orphan that fd; truncating in place works because O_APPEND
seeks to EOF before each write, so the next append lands at the new
EOF post-rotation.

Best-effort: any exception in the rotation path is swallowed so a
broken rotate never kills the build server.
The watcher was being launched by the devcontainer's postStartCommand,
so it ran whenever the container was up — even if no editor was
attached. Agents that don't edit through VS Code paid the CPU cost
and the false-build-failure noise (mid-edit saves the agent didn't
make).

Now:
- Container start does a one-shot compile so binaries are fresh, then
  idles. No watcher.
- ``.vscode/tasks.json`` gains a "Watch & rebuild backend" task with
  ``runOn: folderOpen`` so VS Code starts the watcher when the
  workspace opens and stops it when VS Code closes.
- Agents can opt-in explicitly with ``./scripts/build/_build-server --watch``.
Until now telemetry.jsonl was CLI-only. Per-component .log files
(build-server.log, fsharp-tests.log, packages-canvas.log, …) have
been the only trail for humans, and the only trail at all for
build/test events. Agents had to know which log to grep for what.

Expanding the existing Telemetry mechanism to cover build + tests:

- ``scripts/build/_build-server`` gains an ``emit_event`` helper and
  fires ``build.compile.start|end|fail`` per cycle and
  ``build.initial.start|end|fail`` for the cold compile, including
  ms timings.
- ``backend/tests/Tests/Tests.fs`` now ``Telemetry.init``s the same
  ``rundir/logs/telemetry.jsonl`` and emits ``test.suite.start|end``
  with the exit code.

The per-component .log files keep being written exactly as before
(humans tail them for streaming visual feedback). The .jsonl is the
structured shadow — one file, JSON-per-line, greppable by ``event``
or ``ctx``. Format already matches what the F# Telemetry module and
Dark-side traces use.
Reflects the changes from this branch:
- LibExecution split into Language / Runtime / DarkTypes (umbrella).
- BwdServer disabled at sln/fsproj level (source still on disk).
- Watcher only runs when VS Code is attached; agents compile via
  ``_dotnet-wrapper`` or opt into ``_build-server --watch``.
- ``build-server.log`` auto-rotates above 5 MB.
- ``telemetry.jsonl`` is now the unified structured event log,
  fed by CLI / build-server / Tests entry points; per-component
  .log files keep streaming for humans tailing them.
- ``package-ref-hashes.txt`` moved to ``backend/src/Language/``.
Defensive: a leftover obj/ cache from the pre-split layout can briefly
recreate the old ``backend/src/LibExecution/package-ref-hashes.txt``
file before the next clean build. Ignoring both paths means the file
won't sneak into a commit while the cache rolls over.
The split-LibExecution commit moved package-ref-hashes.txt to
``backend/src/Language/`` but missed three hardcoded references to
the old ``LibExecution/`` path that only show up at runtime:

- ``LocalExec/PackageRefsGenerator.fs`` and
  ``LibPackageManager/PackageRefsGenerator.fs`` both write the hash
  file via a hardcoded ``../LibExecution/package-ref-hashes.txt``
  source-tree path. ``./scripts/build/reload-packages`` would happily
  regenerate hashes into the now-empty old location and the freshly-
  built CLI would read from the new location, so the runtime saw a
  zero-hash map → "Function ... couldn't be found" on every command.
- ``Language/PackageRefs.fs`` looks up the embedded resource by
  ``LibExecution.package-ref-hashes.txt``, but after moving to the
  Language assembly the manifest name is ``Language.package-ref-hashes.txt``.
  Only matters for published / AOT builds (the source-tree path wins
  in dev), but a real bug nonetheless.

All three updated to point at ``Language/``. ``reload-packages``
now writes to the right place and the embedded-resource fallback
finds its bytes.

Caught while exercising the new setup against the CLI; the kind of
hardcoded-path landmine ``git mv`` doesn't surface.
Fantomas's preferred shape now that the strings fit on one line
(post-rename from ``../LibExecution/...`` to ``../Language/...``).
Caught running ``./scripts/formatting/format check`` before the
B.1 baseline commit.
- Snapshot from ./scripts/run-local-exec bench appended to
  benchmarks/results/history.jsonl.
- Captured the rows in scratch/ply-replacement/baseline.md (gitignored)
  with framework-drift notes.
- Patched scratch/ply-replacement/10-baseline.md so the next agent
  uses the bench-script flow instead of the retired
  --filter-test-list measurement invocation.
- Updated README state and progress log.
When ``run-in-docker`` is called from a non-TTY context whose stdin
is a socket that never sees EOF — exactly the shape of a Claude
``run_in_background`` Bash invocation — the existing
``cat <&0 | fix_dir_stdin | docker exec -i ...`` pipeline blocks
forever on the ``cat``: stdin never closes, so ``cat`` never finishes,
so docker exec sits idle behind it, so the build never starts. Two
B.3 build attempts hung this way before the diagnosis stuck.

Branched the non-TTY case in two:

- pipe / regular file / non-empty stdin → keep the existing
  ``cat`` pipeline, which is the path interactive shells and
  ``./scripts/run-in-docker expect ...`` use.
- everything else (``/dev/null``, empty socket, anything where
  ``[-p][-f][-s]`` are all false) → skip the cat and just
  ``docker exec -i ... </dev/null``.

Smoke-tested with ``./scripts/run-in-docker echo hi </dev/null`` and
with the same agent-runtime invocation that previously hung.
Upstream tracing PR (`6370b2335 Tracing: merge trace_fn_results +
trace_fn_arguments into trace_fn_calls`) merged two tables into one;
``internal.dark:17`` still hardcoded the pre-merge count of 23. Actual
count today is 21 (verified via ``sqlite3 rundir/test-data.db .tables``).

Caught running the full backend suite while landing T.1 — passed
under the pre-rebase tip but errored on the rebased tip.
T.1 in scratch/ply-replacement (re-scoped: the original target —
LibExecution/Dval.fs — has no uply blocks anymore after the
blobs-and-streams cleanup; the hot path moved to Runtime/Stream.fs).

Changes:
- Prelude/Ply.fs: add ``Ply.ofTask`` for the reverse bridge. The
  plan presumed both directions exist; only ``Ply.toTask`` did.
- Runtime/Stream.fs: swap ``pullImpl``, ``readNext``, ``readChunk``
  from ``uply { } : Ply<...>`` to ``task { } : Task<...>``.
- The inner ``next`` of ``newChunked`` stays Ply because
  ``StreamImpl.FromIO``'s ``next`` field is still ``unit -> Ply<...>``;
  cascading that field type is a later chunk. Bridges
  (``|> Ply.toTask``) at the FromIO/Mapped/Filtered call sites
  inside ``pullImpl`` keep the inputs flowing.
- Removed a ``return`` inside a ``while`` loop in ``readChunk``'s
  fallback path that ``task { }`` (stricter than ``uply { }`` about
  early returns inside loops) wouldn't accept; ``Exception.raiseInternal``
  still throws so the behavior is identical.
- Tests/Stream.Tests.fs: stripped ``|> Ply.toTask`` bridges that
  the tests added when the functions returned Ply — they now
  return Task directly.

Build: 32 s. Tests: 10 134 / 10 134 passing (after the trace-table-
count fix in the immediately-prior commit).
T.2 in scratch/ply-replacement. Six builtin ``fn`` bodies converted
from ``uply { } : Ply<Dval>`` to ``task { } |> Ply.ofTask``:

- streamFromList, streamUnfold, streamNext, streamToList,
  streamToBlob, streamMap

The ``fn`` field's type ``BuiltInFnSig`` still requires Ply (T.13
swaps the signature itself), so each task body is bridged with
``|> Ply.ofTask`` at the boundary.

Inner callbacks stay uply because their types are dictated by
``StreamImpl`` in ``RuntimeTypes.fs``: ``newFromIO``'s ``next``
parameter is ``unit -> Ply<Option<Dval>>`` and ``Mapped(_, fn, _)``'s
``fn`` is ``Dval -> Ply<Dval>``. Cascading those is a separate chunk.

Inside task bodies, ``let! v = somePly()`` now needs explicit
``|> Ply.toTask`` (the F# task builder doesn't bind Ply natively the
way Ply's uply binds Task). ``Ply known`` constants become
``Task.FromResult known``.

Build 32 s. **10 134 / 10 134 backend tests passing.**
T.3 in scratch/ply-replacement. Two outer ``BuiltInFnSig`` bodies
swapped from ``uply { } : Ply<Dval>`` to ``task { } |> Ply.ofTask``:
``httpClientRequest`` and ``httpClientStream``.

Inner blocks staying uply (constrained by external types):
- ``Ply.List.mapSequentially`` callbacks (Ply-typed contract)
- ``nextChunk`` callback for ``Stream.newChunked`` (FromIO callback type
  in RuntimeTypes is still ``int -> Ply<Option<byte[]>>``)

The nested ``uply { match reqHeaders, method with ... }`` inside
``httpClientRequest`` flattened to ``task { ... }`` — its only Ply
binding is the outer-scope ``makeRequest`` which already returns
``Task<RequestResult>`` natively, so the rewrite is a no-op shape
change.

``Blob.readBytes state bodyRef`` and the ``Ply.List.mapSequentially
... |> Ply.map Result.collect`` chain bridge with ``|> Ply.toTask``;
``makeRequest`` and ``openStreamingRequest`` already return ``Task<_>``
so they bind directly.

Build 40 s. **10 134 / 10 134 backend tests passing.**
T.4 in scratch/ply-replacement. Bench snapshot appended on
``ply-to-task`` after T.1–T.3 (hot-path Ply→Task swap covering
``Runtime/Stream.fs`` + ``BuiltinExecution/Libs/{Stream,HttpClient}.fs``).

Every row is within GC noise of the baseline; the explicit 10 MB
streaming-shaped scenario (``streamToBlob 10 MB``) shows 0.02 %
difference on 20 MB allocated. The biggest delta in the table is
−1.9 % on ``manyBlobs 10000×256 B``, which is jitter.

Conclusion per the T.4 decision rule: the hot-path swap is neutral on
allocations, which is expected (``task { }`` and ``uply { }`` are both
struct-state-machine builders). Continue to T.5; the AOT payoff lives
at the trim-graph end of the plan, not here.

Detail in ``scratch/ply-replacement/iterations/01-task.md`` (gitignored).
T.5 in scratch/ply-replacement. ~21 ``BuiltInFnSig`` outer bodies
across six files swapped from ``uply { } : Ply<Dval>`` to
``task { } |> Ply.ofTask``:

- ``Builtins.fs``      (1)  — ``getBuiltins`` simplified to ``Ply v``
                              since its body is sync (no awaits).
- ``Base64.fs``        (2)  — ``base64Encode`` + ``base64UrlEncode``.
- ``String.fs``        (2)  — ``stringFromBlob`` (both versions).
- ``NoModule.fs``      (2)  — ``debug`` + ``toRepr``. ``Exe.dvalToRepr``
                              already returns ``Task<string>``, so it
                              binds in task context without ``|> Ply.toTask``.
- ``Crypto.fs``        (5)  — sha256/sha384/md5/sha256hmac/sha1hmac.
- ``Blob.fs``          (7)  — every ``Blob.readBytes``-binding builtin.

Inner uply blocks remaining (intentionally deferred to T.13's
``BuiltInFnSig`` swap):

- ``Stream.fs``: 5 callbacks for ``Stream.newFromIO`` / ``newChunked`` /
  ``Mapped`` / ``Filtered`` (callback types in RuntimeTypes still
  ``-> Ply<...>``).
- ``HttpClient.fs``: 3 ``Ply.List.mapSequentially`` / ``nextChunk``
  callbacks bound to Ply-typed slots.
- ``Json.fs``: 4 helpers used by ``Ply.List.flatten`` /
  ``Ply.List.mapSequentially`` and the recursive ``convert`` whose
  signature is ``... -> Ply<Dval>``.
- ``List.fs``: 2 internal sort-comparator helpers with explicit
  ``: Ply<unit>`` signatures.

Build 33 s. **10 134 / 10 134 backend tests passing.**
T.6 in scratch/ply-replacement. Single ``BuiltInFnSig`` body in
``BuiltinHttpServer/Libs/HttpServer.fs`` — ``httpServerServe`` —
swapped from ``uply { } : Ply<Dval>`` to ``task { } |> Ply.ofTask``.
The two helpers it awaits (``executeHandler`` and
``Http.Response.toHttpResponse``) already return ``Task<_>`` so the
inner ``let!`` binds are no-op shape changes.

The inner ``Func<HttpContext, Task>(fun ctx -> task { ... })``
ASP.NET request handler was already a `task { }` — unchanged.

Build 43 s, **10 134 / 10 134 backend tests passing**.
T.7 (partial — BuiltinCli portion). 16 outer ``BuiltInFnSig`` bodies
swapped from ``uply { } : Ply<Dval>`` across six files:

- ``Time.fs``        (1) — task wrap (``do! Task.Delay``).
- ``Posix.fs``       (1) — ``posixFdWrite`` task wrap with
                            ``Blob.readBytes |> Ply.toTask``.
- ``Environment.fs`` (1) — ``getBuildHash`` collapsed to ``Ply v``
                            (sync, no awaits).
- ``Directory.fs``   (3) — all collapsed to direct ``Ply`` (sync).
- ``Process.fs``     (3) — all collapsed to direct ``Ply`` (sync).
- ``File.fs``        (7) — split: 3 task-wraps for the async file IO
                            (``ReadAllBytesAsync``, ``WriteAllBytesAsync``,
                            ``AppendAllTextAsync``), 4 collapsed to
                            ``Ply`` (sync attribute checks).

Pure-sync bodies use ``... |> Ply`` rather than ``task { return X }
|> Ply.ofTask`` — no point allocating a state machine for an
already-synchronous result.

Build 32 s, **10 134 / 10 134 backend tests passing**.

T.7 continues in the next iteration with BuiltinCliHost / BuiltinPM /
BuiltinCloudExecution (94 uply blocks across those three projects).
T.7 (continued — BuiltinCliHost portion). 13 outer ``BuiltInFnSig``
bodies + 1 internal helper migrated across three files:

- ``Canvas.fs`` (5)  — DBCreate / GetOrCreateForAccount / DBListAll
                       / DBDrop / UserGetByName. The DBListAll body
                       has a Ply.List.mapSequentially callback that
                       stays uply per the Ply contract.
- ``Traces.fs`` (6)  — cliTracesList / cliTracesView / cliTracesListByFn
                       / cliTracesGetInput / cliTracesClear, plus the
                       ``loadFnCalls`` private helper (also migrated
                       to ``Task<Dval>`` since its only caller is now
                       in task context).
- ``Cli.fs`` (2 outer; 3 helpers retained as Ply) — cliParseAndExecuteScript
                       and cliEvaluateExpression converted; their
                       ``parseCliScript`` / ``loadCanvasAndDBs``
                       / ``execute`` helpers stay Ply and bridge with
                       ``|> Ply.toTask`` at the call sites.

All ``Sql.executeAsync`` / ``Sql.executeRowAsync`` / ``Canvas.*`` /
``Account.getUserByName`` calls already return ``Task<_>``, so most
inner ``let!`` binds were no-op shape changes. The Ply-returning
helpers in Cli.fs needed explicit ``|> Ply.toTask`` bridges.

Build 48 s, **10 134 / 10 134 backend tests passing**.

T.7 continues with BuiltinPM (58) and BuiltinCloudExecution (18).
T.7 (continued — BuiltinCloudExecution portion). 15 outer
``BuiltInFnSig`` bodies in ``Libs/DB.fs`` swapped from
``uply { } : Ply<Dval>`` to ``task { } |> Ply.ofTask``:

- dbSet / dbGet / dbGetMany / dbGetExisting / dbGetManyWithKeys
- dbDelete / dbDeleteAll / dbGetAll / dbGetAllWithKeys / dbCount
- dbKeys
- dbQuery / dbQueryWithKey / dbQueryOne / dbQueryCount

All bind ``UserDB.*`` helpers; mix of Task-returning
(delete / deleteAll / getAllKeys / count) and Ply-returning
(set / getOption / getMany / getManyWithKeys / getAll /
executeCompiledQuery). The Ply ones bridge with ``|> Ply.toTask``;
the Task ones bind directly. ``compileQueryLambda`` (also Ply)
bridges similarly at each query call site.

Three internal helpers stay uply: ``resolveLoadValues``,
``compileQueryLambda``, plus a nested ``Ply.List.mapSequentially``
callback inside ``resolveLoadValues``. They have explicit
``Ply.Ply<...>`` signatures and are called from the converted
outer bodies via the ``|> Ply.toTask`` bridge — cascading them
into Task is left for T.13 (``BuiltInFnSig`` / ``DvalTask`` flip).

Build 45 s, **10 134 / 10 134 backend tests passing**.

T.7 continues with BuiltinPM (58 blocks).
T.7 (continued — BuiltinPM, partial). 10 outer ``BuiltInFnSig``
bodies converted to ``task { } |> Ply.ofTask`` across four files:

- ``Seed.fs``    (1) — pmSeedExport.
- ``Merge.fs``   (2) — scmMerge / scmCanMerge.
- ``Rebase.fs``  (2) — scmRebase / scmGetRebaseConflicts.
- ``Scripts.fs`` (5) — pmScriptsList / pmScriptsGet / pmScriptsAdd
                       / pmScriptsUpdate / pmScriptsDelete.

All inner ``LibPackageManager.*.X`` and ``Scripts.*`` calls already
return ``Task<_>`` so `let!` binds were no-op shape changes — no
explicit ``|> Ply.toTask`` bridges needed in any of these.

Build 32 s. **10 134 / 10 134 backend tests passing**.

T.7 continues with BuiltinPM Branches (9) / PackageOps (13) /
Dependencies (6) / Packages (20) — 48 uply blocks remaining in
the chunk.
T.7 (continued — BuiltinPM, partial). 13 outer ``BuiltInFnSig`` bodies
swapped from ``uply { } : Ply<Dval>`` to ``task { } |> Ply.ofTask``:

- ``Dependencies.fs`` (4 outer)  — depsGetDependents / depsGetDependencies
                                    / depsGetDependentsBatch
                                    / depsResolveLocations.
                                   The ``Ply.List.flatten`` callback inside
                                   depsResolveLocations stays uply
                                   (Ply contract) and bridges via
                                   ``|> Ply.toTask``. The ``getLocationAny``
                                   private helper also stays Ply.
- ``Branches.fs`` (9)             — scmBranchCreate / scmBranchList /
                                    scmBranchListAll / scmBranchGet /
                                    scmBranchGetByName / scmBranchRename /
                                    scmBranchDelete / scmBranchArchive /
                                    scmBranchUnarchive.

All ``LibPackageManager.Branches.*`` and ``LibPackageManager.Queries.*``
calls already return ``Task<_>``, so binds were no-op shape changes
except inside ``depsResolveLocations`` where a
``Ply.List.flatten``-driven Ply pipeline kept its Ply shape and bridges
once at the boundary.

Build 32 s. **10 134 / 10 134 backend tests passing**.

T.7 continues with BuiltinPM PackageOps (13) + Packages (20) — 33 uply
blocks remaining.
T.7 (continued — BuiltinPM PackageOps). 13 outer ``BuiltInFnSig``
bodies converted:

- pmStabilizeHashes (sync — collapsed to ``... |> Ply``).
- scmAddOps / scmCommit / scmDiscard (try/with patterns; task wrap).
- scmGetRecentOps / scmGetWipOps / scmGetWipSummary / scmGetWipItems
  / scmGetWipOpCount / scmGetCommitCount / scmGetCommits /
  scmGetCommitsForBranchChain / scmGetCommitOps (all simple
  ``Queries.X`` binds).

All ``LibPackageManager.Inserts.*``, ``LibPackageManager.Queries.*``,
and ``LibPackageManager.WipRefresh.*`` calls return ``Task<_>`` —
no Ply.toTask bridges needed.

Build 32 s, **10 134 / 10 134 backend tests passing**.

T.7 continues with BuiltinPM/Libs/Packages.fs (20 blocks — last file).
T.7 (final BuiltinPM piece). 20 outer ``BuiltInFnSig`` bodies in
Packages.fs swapped to ``task { } |> Ply.ofTask``: pmGetStats,
pmFindType / pmGetType, pmFindValue / pmGetValue,
pmFindValuesByValueType, pmEvaluateValue, pmFindFn / pmGetFn,
pmSearch, pmGetLocationsByType / Value / Fn, pmGetAllPreviousHashes,
pmPropagate, pmAtomicUndo, pmGetDeprecationSets,
pmGetCurrentDeprecation.

Bridge work:
- ``LibPackageManager.Stats.get``, ``PMPT.Type/Value/Fn.find``,
  ``pm.getType/getValue/getFn``, ``RTPM.Value.findByValueType``,
  ``PMPT.search``, ``pm.get*Locations`` all return Ply, so each
  ``let!`` site got an explicit ``|> Ply.toTask``.
- ``Execution.executeExpr`` and
  ``LibPackageManager.Queries.getDeprecationSets`` already return
  ``Task<_>``, so no bridge there.
- ``LibPackageManager.Queries.getAllPreviousHashes``,
  ``LibPackageManager.Inserts.insertAndApplyOps``,
  ``LibPackageManager.Propagation.propagate``,
  ``LibPackageManager.Inserts.findCommittedHash``, ``Branches.*``
  all already return Task.
- The ``match`` inside pmAtomicUndo had an inner uply branch that
  produced ``Ok targetHash`` — switched to ``task { return Ok ... }``
  to match the surrounding task block.

Build 29 s, **10 134 / 10 134 backend tests passing**. T.7 complete:
58/58 BuiltinPM blocks migrated; 110/110 across BuiltinCli +
BuiltinCliHost + BuiltinPM + BuiltinCloudExecution since T.7 started.
- changed `and DvalTask = Ply<Dval>` → `Task<Dval>` in Runtime/RuntimeTypes.fs
- bulk-converted Ply constructor sites at builtin fn-field tail positions
  to Task.FromResult across BuiltinExecution/, BuiltinCli/, BuiltinCliHost/,
  BuiltinCloudExecution/, BuiltinPM/, BuiltinHttpServer/, BuiltinDarkInternal/
- migrated BuiltinDarkInternal outer bodies (uply { } → task { })
- migrated tests/TestUtils/LibTest.fs
- 10 134 / 10 134 backend tests passing
- StreamImpl.FromIO `next`/`nextChunk`, Mapped.fn, Filtered.pred are
  now Task<...> in Runtime/RuntimeTypes.fs
- Runtime/Stream.fs newFromIO/newChunked signatures + the inner
  byte-carry `next` task block; pullImpl/readChunk lose their
  Ply.toTask bridges
- BuiltinExecution/Libs/Stream.fs cascades: streamFromList,
  streamUnfold, streamMap, streamFilter callback closures
- BuiltinExecution/Libs/HttpClient.fs: nextChunk body
- LocalExec/BenchmarkScenarios.fs: streamToBlob harness
- Stream.Tests.fs + Blob.Tests.fs: list/chunk pull-fn helpers
  plus the in-test predicate/map closures
- 10 134 / 10 134 backend tests passing
- readBytes / promote return Task<...>
- promoteWalk extracted to top-level `let rec` because the F# task
  builder rejects `let rec` inside resumable code (FS3511)
- two internal bridges remain pending PackageManager-record
  conversion: state.blobs.get and the insert callback (used
  via do!) still come in as Ply
- callers across BuiltinExecution/Libs/{Base64,Blob,Crypto,
  HttpClient,String}, BuiltinCli/Libs/{File,Posix}, and the two
  test files drop their `|> Ply.toTask` bridges
- 10 134 / 10 134 backend tests passing
- getFnBody / partialEvaluate return Task<...>
- two inner Ply.toTask bridges remain pending PackageManager
  record + Interpreter migration
- sync call sites use .Result directly (drop |> Ply.toTask)
- 10 134 / 10 134 backend tests passing
- executionPointToString, callStackString, rteToString return Task<string>
- inner Ply.List.mapSequentially -> Task.mapSequentially
- one Ply.ofTask bridge in LibCloudExecution/CloudExecution.fs
  for the still-Ply extraMetadata helper
- 10 134 / 10 134 backend tests passing
- 10 top-level helpers flipped to Task<...>: unifyValueType,
  unify, resolveType, checkFnParam, checkFnResult, and the five
  DvalCreator helpers
- Ply.List.* → Task.* throughout
- added Task.foldSequentiallyWithIndex to Prelude/Task.fs
- 16 |> Ply.toTask bridges added for callees still on Ply
  (Types.find, TypeReference.{unwrapAlias, toVT})
- task builder doesn't allow early `return raiseRTE` in if-no-else
  the way uply does; dropped `return` keyword (raiseRTE throws
  inline, semantically identical)
- one test (Serialization.DarkTypes.Tests.fs) drops a Ply.toTask
- 10 134 / 10 134 backend tests passing
- executeInner / execute return Task<Dval>
- inner uply blocks -> task; Ply <expr> -> Task.FromResult <expr>
- Ply.List.* -> Task.*
- 6 |> Ply.toTask bridges for still-Ply callees (fns.package,
  fns.isHarmful, values.package, TypeReference.toVT)
- 5 sites: drop `return` on raiseRTE in task to avoid early-return
  unit/generic mismatch (semantically identical -- raise throws)
- RTQueryCompiler.fs Interpreter.execute caller drops Ply.toTask
- Interpreter.Tests.fs drops 2 Ply.toTask bridges
- 10 134 / 10 134 backend tests passing
- Runtime/RuntimeTypes.fs ExceptionReporter, Notifier, consoleReporter,
  consoleNotifier
- LibCloudExecution/CloudExecution.fs extraMetadata/notify/sendException
  (extraMetadata's prior Ply.ofTask bridges collapse)
- Cli/Cli.fs, LibPackageManager/Seed.fs, LocalExec/BenchmarkScenarios.fs
  in-test notify/sendException
- tests/TestUtils/TestUtils.fs exceptionReporter + notifier
- 10 134 / 10 134 backend tests passing
- RT.PackageManager record callbacks (getType/Value/Fn, getBlob,
  persistBlob, isHarmful, init) now Task
- Types/Values/Fns/Blobs ExecutionState helper records: callbacks
  Task
- Types.find and TypeReference.{unwrapAlias, toVT} flipped to Task
- Task.NEList.mapSequentially added to Prelude/Task.fs
- 16+ |> Ply.toTask bridges dropped at now-Task call sites
- LibPackageManager.PackageManager.rt bridges PMRT.Type/Fn/Value.get
  (still Ply) with |> Ply.toTask at field assignments
- PT2RT.PackageManager.toRT bridges Ply→Task for the three lookups
- BuiltinExecution.Libs.Stream resolveElemVT/KT flipped to Task
- BuiltinExecution.Libs.Json's convert (still Ply) wraps now-Task
  TypeReference.toVT and Types.find with |> Ply.ofTask
- Blob.promote's insert parameter flipped to Task<unit>; tests use a
  pmInsertTask adapter for the still-Ply PMBlob.insert
- 10 134 / 10 134 backend tests passing
- 11 PT.PackageManager callbacks flipped: findType/Value/Fn, search,
  getType/Value/Fn, getTypeLocations/getValueLocations/getFnLocations,
  init
- PackageManager.empty + withExtras converted
- LibPackageManager.PackageManager: rt's PMRT bridges + pt's PMPT
  bridges per field (still-Ply backends), createInMemory and combine
  use Task.FromResult / task { } for the merge paths
- combine's search-merge needs explicit SearchResults annotation on
  each let! to disambiguate record fields through the Task path
- PT2RT.PackageManager.toRT collapses its old Ply.toTask bridges
- LibParser.NameResolver: 3 resolve helpers wrap pm.find* with
  Ply.ofTask for the still-Ply resolveGenericName helpers
- LibPackageManager.DeferredResolver: 7 walker sites wrap pm.find*
  with Ply.ofTask
- BuiltinPM.Libs.Packages drops 6 |> Ply.toTask bridges
- tests/TestValues.fs flips Ply -> Task.FromResult on PM-overlay
  field constructors
- 10 134 / 10 134 backend tests passing
- LibPackageManager/RuntimeTypes.fs: Type/Value/Fn.get,
  findByValueType, Blob.get/insert/sweepOrphans
- LibPackageManager/ProgramTypes.fs: findItem/getItem/getItemLocations
  and the Type/Value/Fn/search public wrappers
- Caching.withCache flipped Ply -> Task to match both backends
- LibPackageManager.PackageManager.{rt,pt} drop their
  |> Ply.toTask bridges around withCache results
- Propagation.fs ItemProcessingContext<'T> callbacks (getItem,
  getLocations) typed Task
- BuiltinPM.Libs.Packages drops 5 Ply.toTask bridges
- tests/Tests/Blob.Tests.fs drops 9 Ply.toTask bridges
- 10 134 / 10 134 backend tests passing
- DeferredResolver.fs: 20 uply -> task; reResolveNameResolution +
  reResolveTypeName/Fn/Value + AST walkers (TypeRef/StringSegment/
  MatchCase/PipeExpr/Expr) flipped to Task<...>; Ply leaves
  collapsed to Task.FromResult; the temporary Ply.ofTask bridges
  earlier T.8 inserted around pm.find* are gone
- Stats.fs::get and PackageRefsGenerator.fs::generate flipped
- WipRefresh.fs: 3 |> Ply.toTask bridges around DR.reResolve* sites
  collapsed
- BuiltinPM/Libs/Packages.fs::pmStats: explicit
  `(stats : LibPackageManager.Stats.Stats)` annotation to
  disambiguate stats.types from RT.Types.types
- LibPackageManager/ is now Ply-free outside of the Ply package
- 10 134 / 10 134 backend tests passing
…are/tests/LocalExec/Cli to Task

Single-pass bulk substitution across the remaining tree:

- Ply<...> -> Task<...>, uply { } -> task { }, Ply.List.foo ->
  Task.foo, Ply.NEList.foo -> Task.NEList.foo, Ply.map/bind ->
  Task.map/bind, Ply(x) -> Task.FromResult(x), |> Ply ->
  |> Task.FromResult; stripped |> Ply.toTask and |> Ply.ofTask
  bridges that collapsed to identity
- Auto-added `open System.Threading.Tasks` to every file that
  referenced Task and lacked the import

Surgical fixes after the bulk pass:

- Record-disambiguation annotations in
  LibParser/WrittenTypesToProgramTypes.fs (8 sites — F#'s task
  builder pins TOverall outermost-in, so record literals shared
  by multiple types resolve via closest-by-name; uply was
  more permissive)
- let rec extractPath extracted out of a task { } block (FS3511)
- Canvas.fs (CliHost): `Ply "unknown"` -> `Task.FromResult "unknown"`
- TestUtils.testManyPly drops `>> Ply.toTask` chain

Tree is now Ply-free except `Prelude/Ply.fs` itself + the one
`let uply = Ply.uply` re-export in Prelude.fs (both retired in T.14).

10 134 / 10 134 backend tests passing.

README + 30-track-valuetask.md updated to: (1) prescribe
bulk-pass methodology for future migrations of this shape, and
(2) re-anchor V to fork from the tip of T (cleaner V diff,
B/T/V three-way comparison still preserved in comparison.md).
- rm backend/src/Prelude/Ply.fs
- backend/paket.dependencies: drop `nuget Ply = 0.3.1`
- 10 paket.references files: drop `Ply` line
  (Prelude/Runtime/DarkTypes/Language/LibExecution/
  BuiltinCli/CliHost/Execution/PM/HttpServer/Cli)
- paket install: removes Ply (0.3.1) +
  System.Threading.Tasks.Extensions (4.6.3 — transitive)
- ~25 files: strip `open FSharp.Control.Tasks` and
  `open FSharp.Control.Tasks.Affine.Unsafe` (Ply-package
  namespaces; F# 10's task { } is in framework)
- backend/src/Prelude/Prelude.fs: drop the `type Ply<'a> = …`
  / `let uply = Ply.uply` re-exports
- 5 FS3511 (let rec inside task { } resumable code) fixes:
  lift recursive helpers above the outer task block
  - LibExecution/Execution.fs::groupConsecutiveWithCounts
  - LibPackageManager/WipRefresh.fs::processOps
  - LibPackageManager/Propagation.fs::discoverDependentsLoop
  - tests/TestUtils/TestUtils.fs::executionStateFor
    (let rec exceptionReporter outside the outer task)
- BuiltinExecution/Libs/HttpClient.fs SocketBasedHandler.handler:
  vtask { } (provided by Ply pkg) -> task { … } wrapped in
  ValueTask<Stream>(inner) for the ConnectCallback signature
- LibCloudExecution/CloudExecution.fs::extraMetadata: explicit
  (result : Metadata) annotation (Metadata is a tuple-list alias
  whose elements aren't all strings, so F# inferred wrong)
- Stale "stays uply" comments in 4 files deleted
- 30-track-valuetask.md: link to Darklang's "Optimizing F# tasks"
  blog post as required prior reading for V

`grep -rn 'uply\|Ply\.Ply\|open Ply' backend/src` returns nothing.
10 134 / 10 134 backend tests passing.
Now that the Ply→Task migration removed the thread-affine-lock
hazard (Monitor.Exit throwing on Ply continuation hops), the
single-consumer invariant on `DStream` can be enforced cleanly.

- Runtime/Stream.fs: Finalizer carries a permit-1 SemaphoreSlim
  on its lockObj
- readNext / readChunk: Wait(0) at entry, raise
  "concurrent consumer on a single-consumer DStream" on contention,
  release in a `finally` so a raised callback can't strand the
  lock. Used Wait(0) (not WaitAsync) because the task body needs
  the result synchronously to decide whether to short-circuit
- Finalizer.Finalize disposes the SemaphoreSlim to release the
  underlying handle
- new test: concurrentReadNextRaises (Stream.Tests.fs) parks
  first readNext at a gated callback, asserts second raises,
  confirms first still completes after the gate

10 135 / 10 135 backend tests passing.
The release build's resumable-code analyzer (under PublishTrimmed)
can't always statically reduce complex recursive task patterns.
Specifically Runtime/TypeChecker.fs::unifyValueType — `match!` over
`Types.find` (Task-returning) inside a recursive task. The compiler
emits FS3511 and falls back to a dynamic-dispatch state machine,
which is correct but slightly slower than the inlined version.

`--warnaserror` was turning that into a hard build failure. The
restructure to make the analyzer happy would split unifyValueType
in invasive ways for the central type-checker entry point — not
worth it. nowarn:3511 is the standard workaround.

Documented inline. Affects Runtime/TypeChecker.fs and a couple of
similarly-shaped recursive-task helpers.

Verification chunks T.16/T.17/T.18 all green:
- 10 135 / 10 135 backend tests passing
- Release exe size B 76,416,031 -> T 76,797,646 (+381 KB / +0.5%)
- Cold-start steady median (5 runs, same machine, same DB):
  B 0.484s -> T 0.397s (-85 ms / -18%)
- Trim/AOT warnings: IL2 0->0, IL3 0->0, NU1510 60->38 (-22)
- Cli.fs top-level catch walks AggregateException/InnerException
  recursively; prints type-name + message per layer, full stack
  trace from the outermost throw at the bottom. The single
  `e.Message` print collapsed every nested error to the AggregateException
  surface, hiding what actually crashed.
- Db.fs executeRow{,Option}Async swap the literal "fail" exception
  message for "SQL query failed in <fn>: <err.Message>". The
  underlying exn was only ever in the structured metadata, which
  the top-level catch dropped before stringifying.

Independently useful (any startup DB error now surfaces its cause);
load-bearing for the AOT spike, where the binary dies inside
cli.growIfNeeded with the literal string "fail" and zero hint at
which trim-broken Microsoft.Data.Sqlite path is the culprit.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant