# Darklang: a rewrite that is not yet production-ready

> Darklang combines an F# language, a browser editor and a backend so you can build servers and command line tools in one language. The repository you are looking at is not the product people use: production runs Darklang-Classic from a separate repo, and this one, called dark-next in the README, has been under development since February 2023 and is stated as not yet ready for production use.

**darklang/dark** — Darklang main repo, including language, backend, and infra

- Repository: https://github.com/darklang/dark
- Website: https://darklang.com
- Stars: 2,172 · Forks: 115
- Language: F#
- License: Apache-2.0
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/darklang-dark

## Production is a different repository, and this one is still in development

The second paragraph of this README contains the fact that determines everything else about evaluating the project.

The note says that the production version of Darklang, Darklang-Classic, is not in this repo. Since Feb 2023, the Darklang team has been working on a new version of Darklang, which is in this repo, and temporarily it is being referred to as dark-next. And then, plainly: dark-next isn't yet ready for production use.

So the repository with the recent commits, the F# source, the backend and the vscode-extension is a development branch of a product whose users are on a different codebase entirely. Darklang-Classic lives at github.com/darklang/classic-dark and is what you log into at darklang.com.

The elapsed time is the part that gives an evaluator pause. The rewrite started in February 2023, and the last push to this repository was on 2026-09-28, which is more than three and a half years of development later. A rewrite of a language plus editor plus backend is a large undertaking and a three-year timeline is not unreasonable for one. What is notable is that it is still described as not ready for production at that point, while the older version continues to serve users.

The release tags do not contradict that, and reading them clarifies what is actually being versioned. The three most recent are v0.0.37, v0.0.38 and v0.0.39, and all three are named cli-pre-release, on 2026-09-27 and 2026-09-28. So the versioned artefact is a command line tool, it is a preview, and its version is still in the 0.0 range. Three releases in about thirty-six hours is an active release cadence for that one component.

That tells you something about how the project is being built and released during the rewrite: the language and backend are not being versioned per release, but a CLI is, and it is being shipped often. So there is a real, runnable, frequently-updated deliverable in this repository even though the product as a whole is not ready. If you want to see Darklang's current state rather than its promise, the CLI is where to look.

The README is straightforward about the two-channel arrangement. The Discord is where most communication happens, with an invitation to join and say hi, and the GitHub Issues are where most work is tracked. There is also a Darklang-Classic login link and a link to the Classic repository, both in the see-also list, so the README treats the two as coexisting rather than the new one as a replacement.

## The build image version is a git SHA copied by hand between two repos

The Dockerfile in this repository begins with a comment block that is the most useful thing in the file for anyone trying to contribute, and it documents a three-repository dance that is genuinely awkward.

The comment is headed DOCKERFILE_REPO and marked very important. It says the dockerfile is stored in the darklang/dockerfile repository and is copied into darklang/dark. The reason for the copy is that it allows developers to develop Dark directly without pulling a Docker image.

Then the workflow. You can make changes to this file in this repo, and then copy them to darklang/dockerfile. And then the part that costs everyone an hour the first time: to actually use any changes in the image, you need to change the SHA used in config/circleci.yml. You can find the new SHA after pushing to darklang/dockerfile, because the SHA is generated as part of that build. The comment tells you to search for DOCKERFILE_REPO to find where to make that change.

So the chain is: edit the Dockerfile here, copy it to another repository, push, let that repository's build generate a SHA, find the SHA, and hand-edit a CircleCI configuration file in a third place. Four steps across three repositories, one of which is a manual copy and one of which is a manual lookup of a build artefact.

The reason for the split is sensible and worth stating fairly, because it explains the complexity. The Dockerfile is the development environment, and it needs to be identical for everyone and for CI. Keeping the authoritative version in a separate repository means the environment is versioned as an artefact rather than as a file, and the SHA is the version. Keeping a copy here means you can read and locally modify it without checking out the other repository. Both goals are reasonable. The cost is the manual coordination, and the project has documented it thoroughly enough that a contributor will not lose an afternoon to it, which is the right mitigation.

The Dockerfile itself is modern in the ways that matter. The very first line is a syntax directive, docker/dockerfile:1, with a comment above it explaining that the line allows heredocs and must come before any other content. That is the BuildKit syntax directive, and putting it first is required. The base image is ubuntu:24.04 under the alias dark-base, so the current Ubuntu LTS. And there is a TARGETARCH build argument whose comment says it creates variables to allow builds to work on both amd64 and arm64, so the development environment is multi-architecture.

The comment also says this image is used to compile and test Dark, and that later they will use it to create another dockerfile to deploy. So the Dockerfile you are looking at is the build-and-test environment, not the deployment artefact, and the deployment story is a separate, unwritten Dockerfile.

## uid, gid and FORCE_BUILD: build arguments that came from real problems

Two build arguments in this Dockerfile are there because of specific, documented annoyances, and the comments explaining them are better engineering writing than the arguments themselves.

The first pair is ARG uid=1000 and ARG gid=1000, introduced by a comment that says these are reasonable defaults and what the dark uid and gid would be if they had not specified values. The comment then explains the actual purpose, which is about the host filesystem rather than the container: by exposing these as build arguments you can set the values to match the host user's uid and gid, allowing for dark-owned files in the container to be owned by the host user on the host filesystem.

Then the history, which is the interesting part. They didn't need this in OS X because Docker for Mac handles it for you. They avoided it on Linux for a while because often the first non-root user has uid 1000. But that is not always the case, and when it is not, you get files owned by 1000:1000 that need to be sudo chown'd on the host.

That is a complete bug report, a diagnosis and a fix, in a Dockerfile comment. The problem is a very common container annoyance: a container that runs as root or as a hardcoded uid writes files that the host user cannot edit, and the standard workaround is a sudo chown on the host. The fix is to pass the host uid in, which requires the developer to know their own uid, which is why the default is 1000 and why the comment explains when 1000 is wrong.

The second build argument is ENV FORCE_BUILD=8. There is no comment explaining it, and that is the point: a numeric environment variable with a number in it, in a base image, is a cache-busting counter. Docker layers are cached, and a RUN apt update will not re-execute if the layer and its inputs are unchanged, which means a base image can serve indefinitely stale package lists. Incrementing a value that is not otherwise used invalidates every layer after it. FORCE_BUILD=8 means this has been done eight times.

That is a well-known workaround with an unfortunate property: it is invisible in a diff, because the only change is a number, and nobody reading the Dockerfile knows when the last rebuild happened or what prompted it. A build-arg or a version label would be more discoverable. But it works, and it is a single line.

The apt invocation itself is written with care. DEBIAN_FRONTEND=noninteractive is set inline with the command rather than as an ENV, and the update uses --allow-releaseinfo-change, which is the flag you need when apt's release information has expired in a cached image. Both of those are the result of having built on a long-lived cached image before.

## user-code/ at the top level, which is the product idea

The repository listing is the best documentation of what Darklang actually is, and one directory name in it explains the product better than the README does.

The top level is .circleci/, .claude/, .devcontainer/, .dockerignore, .editorconfig, .fantomasignore, .gitattributes, .github/, .gitignore, .prettierrc.toml, .style.yapf, .vscode/, .yamllint, AGENTS.md, CHANGELOG.md, CLAUDE.md, CODE-OF_CONDUCT.md, CODING-GUIDE.md, CONTRIBUTING.md, Dockerfile, Dockerfile.build-base, LICENSE.md, LICENSES, README.md, backend/, benchmarks/, config/, deploy/, docs/, experiments/, install.sh, packages/, rundir/, scripts/, user-code/, and vscode-extension/.

The one to look at is user-code/. It is a top-level directory, sibling to backend/ and packages/, which means the project treats the code a customer writes as a first-class part of the repository layout rather than as something that lives inside the editor's state or in the customer's own version control.

That is the architectural commitment. Darklang's pitch is a combined language, editor and infrastructure for building backends and command line tools, and a language that shares a repository with its runtime and its tooling is a language where the boundary between system and application is a directory. So a Darklang program is not a set of files you keep elsewhere that a backend compiles and serves; it is code that lives in a defined place in a project structure, alongside the runtime, with the editor operating on it directly.

The consequences run in both directions. It means the whole development loop is one repository checkout and one container, which is why the Dockerfile has to be so carefully pinned and why the devcontainer and vscode directories exist. And it means there is no clean seam for a customer to version their program separately from the language runtime unless Darklang provides one, so the user-code directory is doing the work that a package boundary would do in a conventional language.

The other directories fill in the rest of the shape. packages/ is where distributable units live. benchmarks/ is performance work, which for a language with a runtime and a compiler is a substantial ongoing cost. experiments/ is where the team puts things that are not yet products, and the leading underscore convention is a common way to keep build systems from picking them up. rundir/ is a runtime directory. deploy/ is deployment. config/ holds the CI configuration that the Dockerfile comment sends you to. scripts/ and install.sh are the entry points.

The docs/ directory is present, and so are two guides at the root: CONTRIBUTING.md and CODING-GUIDE.md, with the README linking CONTRIBUTING.md as the guide to contributing. There is a CHANGELOG.md, a CODE-OF_CONDUCT.md, and the licence is at LICENSE.md with a LICENSES/ directory alongside it, which is the REUSE-style layout for projects with third-party licence texts.

## Fantomas, Prettier and yapf: three languages and three formatters

The dotfiles at the root of this repository are a compact inventory of the languages involved, and the formatter choices are the interesting part.

There is .fantomasignore, which is the ignore file for Fantomas, the F# formatter that the F# community standardised on. There is .prettierrc.toml, which is the configuration for Prettier and therefore for the TypeScript and JavaScript in the repository, most of which will be in the vscode-extension/ directory. There is .style.yapf, which is the style configuration for yapf, the Python formatter. And there is .yamllint, .editorconfig and .gitattributes.

So the repository is at minimum F#, TypeScript or JavaScript, Python and YAML, with a dedicated formatter configuration for each of the three code languages. That is a normal setup for a project with a compiler, an editor extension and a set of build and deployment scripts.

The Python choice is the one worth commenting on. yapf is Google's Python formatter, and it is the older of the two Google formatters; the other was black, and the current default in most new projects is ruff's formatter. Choosing yapf in a project started in 2023 is a deliberate signal, because black had been the community default for years by then and ruff was already consolidating linting and formatting. It is most likely a legacy choice carried over from code that predates the rewrite, or a preference for yapf's diff-minimal output. Either way it is a signal that some of this code is not new.

Choosing Fantomas for F# is not a signal of the same kind, because there is effectively one serious F# formatter and the community has converged on it. So the F# side of the repository, which is the majority of it, is formatted to the community standard automatically.

Using Prettier for the TypeScript is also unremarkable and current. So of the three, one is a deliberate outlier and two are the obvious choice.

The remaining root files describe the tooling in a bit more detail. .editorconfig handles indentation and trailing whitespace across all four file types in one place, which is what stops the three formatters from disagreeing about tabs. .yamllint lints the YAML, which matters for a repository with a CircleCI configuration that has to be correct. .gitattributes handles line endings, which is a recurring source of diff noise in a project with contributors on several operating systems.

And there are two agent instruction files, .claude/ and AGENTS.md, plus CLAUDE.md, alongside CONTRIBUTING.md and CODING-GUIDE.md. The CODING-GUIDE.md is the human-facing convention document and the AGENTS.md and CLAUDE.md are the machine-facing ones, so the project maintains the same rules in two forms.

## The open source announcement is a TODO URL, and the repo guide is stale

Two documentation problems in this README, both stated rather than hidden, and both worth an evaluator's attention for different reasons.

The first is the legal-status link. The sentence that establishes the project's licensing is: Darklang is [open source](https://blog.darklang.com/TODO) under the Apache License 2.0. See our LICENSE.md.

The linked blog post is a TODO. It does not exist. So the project's own announcement of its open source status, in the sentence that matters most to anyone deciding whether they may use it, points at an unwritten page.

The substance is not in doubt. The repository licence field is Apache-2.0, there is a LICENSE.md at the root, and there is a LICENSES/ directory which is where a project using the REUSE specification keeps the licence texts for third-party components. So the terms are available and unambiguous. What is missing is the explanation, and a link labelled open source that leads to a TODO is worse for a reader than no link at all, because it invites them to look for terms and then find nothing.

The second is the repository guide, and this one is self-deprecating in a way that is at least honest. The see-also list includes: our guide to the repo for help browsing, though, it's a bit outdated.

The guide is at docs.darklang.com/contributing/repo-layout. So there is a documented map of the repository, the project acknowledges it has drifted, and it has left it in place rather than removing it. That is the right call, because a stale map with a warning is more useful than no map, and because a three-and-a-half-year rewrite with a directory structure this distinctive is genuinely hard to navigate without one.

What a contributor has instead is the root listing and the CODING-GUIDE.md. So the practical guidance is to read the directory names, which are reasonably descriptive, and to accept that the orientation document will help with the big picture and mislead on the details.

The rest of the see-also list is more current and points at the live surfaces. The Discord at darklang.com/discord-invite, where most communication happens, with an invitation to join. The GitHub Issues, where most work is tracked. The Darklang-Classic login and the Classic repository, both linked so a reader arriving at this README from a search for Darklang finds out within a paragraph that this is not the product they are looking for. And the CONTRIBUTING.md link, which is the one piece of guidance the README is confident about.

There is also a Ceasefire Now badge from techforpalestine at the top of the README. That is a values statement rather than an engineering fact, and it is placed prominently, which tells you something about the project that no other file in the repository does.

## Conclusion

Contribute to darklang/dark if you want to work on a language, editor and backend where the language, the runtime and the development environment are one project, and you are willing to work on something the project itself says is not ready for production. Do not plan a production Darklang deployment against this repository, because the production version is Darklang-Classic in a different repository and this one has been in development since February 2023. Do not expect the documentation to orient you quickly either, since the repo layout guide is described in the README as a bit outdated and the blog post linked as the project's open source announcement is a TODO placeholder. Verify four things. Read the README's own framing first, because the sentence about Darklang-Classic not being in this repo is the one that determines whether you are looking at the right code. Check what the release tags mean before pinning: the recent ones are all v0.0.x named cli-pre-release, so the only versioned artefact is a preview CLI and three of those shipped in a day. Read LICENSE.md and the LICENSES/ directory directly for terms, since the README's open source link points at an unwritten blog post. And if you are going to build, read the Dockerfile's DOCKERFILE_REPO comment before anything else, because the build image version is a git SHA that has to be copied by hand from a second repository into the CircleCI configuration. The deciding fact is that this is a patient rewrite of a working product by a team that has kept the old one running rather than replacing it, which tells you more about the project's trajectory than any roadmap would.

## FAQ

### What is Darklang?

It is a combined language, editor and infrastructure for building backends and command line tools, in an F# codebase, with a browser editor and a backend, described in the README as making it easy to build backends and CLIs. The repository is the main repo including language, backend and infra, and the homepage is darklang.com.

### Is darklang/dark the production version of Darklang?

No. The README states that the production version, Darklang-Classic, is not in this repo and lives at github.com/darklang/classic-dark. Since Feb 2023 the team has been working on the version in this repo, temporarily called dark-next, and the README says plainly that dark-next isn't yet ready for production use.

### What do the Darklang release tags mean?

The three most recent are v0.0.37, v0.0.38 and v0.0.39, and all three are named cli-pre-release, on 2026-09-27 and 2026-09-28. So the versioned artefact is a command line tool in preview at version 0.0.x, released several times in a day, while the language and backend as a whole are not released.

### How do I build Darklang?

The Dockerfile is a build-and-test image on ubuntu:24.04 rather than a deployment artefact, and the README's quick route is the install.sh script at the repository root. If you are building the container image, read the DOCKERFILE_REPO comment first, because the file is stored in the darklang/dockerfile repository, copied here for local editing, and using any change requires changing a git SHA in config/circleci.yml.

### What licence is Darklang under?

Apache 2.0, with a LICENSE.md at the repository root and a LICENSES/ directory alongside it. The README links the open source announcement to blog.darklang.com/TODO, which is a placeholder that does not exist, so read LICENSE.md directly rather than following that link for terms.

### What languages and tools does the Darklang repository use?

F# is the primary language, formatted with Fantomas, configured through a .fantamosignore at the root. The TypeScript and JavaScript, most likely in the vscode-extension directory, are formatted with Prettier through .prettierrc.toml. Python is formatted with yapf through .style.yapf, and there is also .yamllint, .editorconfig and .gitattributes.

## Sources

- [darklang/dark on GitHub](https://github.com/darklang/dark)
- [License: Apache-2.0](https://github.com/darklang/dark/blob/main/LICENSE)
- [Project website](https://darklang.com)
- [README](https://github.com/darklang/dark/blob/main/README.md)
- [Releases](https://github.com/darklang/dark/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/darklang-dark
