Working in scratch files — tcm-ts

7. Working in scratch files

Your code lives in the codebase; scratch files are where you write it. This page is the everyday loop: writing, saving, changing, fixing, renaming and undoing.

7.1 Scratch files

Any .ts or .tsx file at the top of your codebase directory is a scratch file (tcm init makes scratch.ts). It's ordinary TypeScript: your editor checks it, and it imports packages without installing them (packages). node scratch.ts runs it when what it imports is installed in the project; tcm check runs its watch expressions either way.

  • Namespaces. // tcm: namespace lib.util as the first line puts the file's definitions under lib.util. Without it they go at the root. tcm add --into lib.util does the same from the command line.
  • Using the codebase. import { square } from "#codebase" imports from the root namespace; #codebase/lib/util from lib.util. Packages and Node builtins import as usual (packages).
  • Watch expressions. An expression on its own line, like square(12), is run and its value shown. It's never stored. Values are cached by hash, except one whose code reaches outside the codebase (the clock, randomness, files, the network, the environment), which is evaluated every time; tcm check --fresh runs them all again.
  • What goes in. Top-level functions, consts, classes, types and interfaces each become a definition. export is optional and makes no difference to tcm. A top-level let is module state (a cache, a counter): its own definition, which what assigns to it shares a component with. A few things are refused with a reason, like top-level var and enums (Node can't run them); see what a definition is.

Once code is saved, you can empty or delete the file. Many people keep one scratch file and reuse it.

7.2 Check, add, update

tcm check (or tcm, which checks every time you save) reports each definition in the file, a line each with its type (cut at 120 characters; tcm view shows all of it):

statusmeaningto save it
newnot in the codebase yettcm add
identicalalready saved, exactly thisnothing to do
updatethe name exists, with different codetcm update
docsame code, a new doc commenttcm add or tcm update
aliasthis exact code is already saved under another nametcm add gives it a second name
resolvesthe name is conflicted (after a merge): saving this resolves it to this definitiontcm update, see merging

tcm add saves new things and never replaces anything: a name that's already taken is skipped with a note. tcm update also replaces, and that's where the codebase does its work. A definition whose code is the same but whose comments changed is saved too (~ text), as is one importing something from outside the codebase by another name (join_1 made join): it has the same hash, and reads as it's written now on this branch, an entry in its history like any change (history). Both import names the code uses and doesn't import, as tcm patch does (said as the import lines added): from the codebase, the one in the file's namespace; else a package or module the file imports from already, if it exports it; else the only definition by that name, or where the codebase's own code imports it from (import os from "node:os", or an alias like isImportDeclaration as ts_isImportDeclaration). tcm check imports them too, writing them into the file (saving would add them anyway, so it checks what will be saved); tcm watch only says them (not imported: …), since the file is open in your editor as you write it. Saving also takes out the imports nothing in the file uses (took out imports nothing uses: randomBytes): one the code stopped using, or of a name that's gone. Used is mentioned anywhere outside the imports: in code, a comment's {@link}, a string, either side of a conflict's markers. An import binding nothing (import "./setup") stays, and so does React in a .tsx file. tcm check leaves an unused import where it is (code using it may be still to be written), saying saving takes it out.

7.3 When an update breaks something

tcm update re-checks everything that uses what you changed. What still typechecks is updated along with it. What doesn't keeps its old version, so the codebase is never half-changed, and goes on the todo list.

A change that keeps a definition's type (its code changed, its signature didn't) can't make anything using it stop typechecking, so what uses it is re-hashed without being checked again, as in Unison: an update takes about as long however much uses what changed. A new type, or a changed class (whose type is more than its signature), is checked through everything that uses it:

$ tcm update
  ~ update    geo.distance : (a: Point, b: Point, unit: "m" | "km") => number  (was #vwvxl53m)

  1 dependent definition(s) no longer typecheck; see `tcm todo`

$ tcm todo
needs fixing geo.pathLength: Expected 3 arguments, but got 2.

fix with `tcm edit <name>`, change the scratch file, then `tcm update`

tcm edit writes a definition back into a scratch file, already pointing at the new versions of what it uses, imported by their names, and says which it imports so (tcm patch does the same):

$ tcm edit geo.pathLength
wrote geo.pathLength to scratch.ts
  distance: imported as geo.distance (the version it used, #vwvxl53m, is replaced)
// tcm: namespace geo
import { distance } from "#codebase/geo";
import type { Point } from "#codebase/geo";

/**
 * How far you travel visiting the points in order.
 */
export const pathLength = (ps: Point[]): number => ps.slice(1).reduce((n, p, i) => n + distance(ps[i]!, p), 0)

That holds however many changes it couldn't follow, and when one hashes as an older version did (a change undone): the version a name has is the current one. Only where nothing names what replaced it (a name removed with --force) is the version it used imported by its hash, from the cache (import { h_… as distance } from "./.tcm/cache/….ts", by a path from the scratch file's directory: ../../cache/ from a session's), and tcm edit says that too. A file importing a version by its hash from a module that isn't there is told so, as the one problem, with what to import instead (uses #vwvxl53m, a version of geo.distance nothing names now: import the current one by its name, from "#codebase/geo"), rather than with the errors following from it.

Fix it (here, pass the unit: distance(ps[i]!, p, "m")) and update:

$ tcm update
  ~ update    geo.pathLength : (ps: Point[]) => number  (was #3ah7ke6j)

$ tcm todo
nothing to do

7.4 Editing what's saved

tcm edit <name>... is also how you change anything: it writes definitions into scratch.ts. A scratch file holding work (anything not saved as it is) keeps it: what you edit next is added to it, its imports with the file's (none twice), and a definition already in it isn't added again. What's in the file saved as it is goes (with imports only it used), unless the rest of the file uses it, so it holds what you're working on. One holding only what's saved starts again. A scratch file's imports are one statement per module, wrapped when long. --file other.ts picks another file, --force replaces what it holds. Code with JSX goes to scratch.tsx, which takes the rest too while it holds work and scratch.ts doesn't. Edit, then tcm update. You can edit several at once, from different namespaces: each gets a // tcm: path a.b.c comment when its path isn't the file's namespace plus its name, so update puts it back where it came from. A // tcm: namespace line further down the file starts a section: the definitions after it go to that namespace. tcm check, add and update take several files at once. For a small change, tcm patch <name> --old … --new … does it in one step, with no scratch file: find-and-replace on the code as tcm view shows it, then saved.

Definitions written into one file are declared in an order that typechecks (a constant before another whose value uses it), and each is saved as it reads alone, as tcm view shows it: where writing them together gave one a name another has (find_1, join_1, Move_1), it's stored with its own. Added to a file holding work, a definition goes before the first of the file's that uses it, and the file's import of it goes; what it uses that the file names otherwise (another limit) is imported by another name there (limit_1), never taken to mean the file's. A function's or class's name is part of its hash (code can read it as .name), so it's never renamed: two declared alike (find from two namespaces) can't be in one file, and tcm edit says to edit the second in a file of its own (--file); a save refuses one declared find_1 that the codebase has as find.

Definitions that refer to each other (functions calling each other, a class and the functions that take it) are hashed together, as one, so they're updated together. tcm edit writes only what you asked for, and says what it's in a cycle with. Saving a change to some of them saves the rest with them, as they are, against the new code (with lib.isOdd: in a cycle with what changed, so saved with it): none goes on using an old one. To change those too, edit them as well. The same goes for a change that makes a cycle: a definition the file imports that uses what the file changes is saved with it when the new code uses it in turn. And one the file imports that uses what it changes, in a cycle or not (an interface of a class it changes, a function taking that class), is re-derived against the new code and saved with it, a dependent (~ updated lib.keep (dependent)): checked alone, the file would be checked against its old version, typed with the old code. So removing a method from a class and its use in another goes through in one save, whatever else is typed with the class. Only what uses what the file changes comes: a type with only a comment changed brings nothing. Definitions brought in so are written after the file's code, and that what a constant's value uses is declared later is no problem there (the module that runs puts each after what it uses); what's wrong in the file's own code is said as it is. tcm update refuses a file that replaces only some of them, since the others would go on using the old ones. A cycle's other members are written importing what they use as the file names it, or by another name where the file binds that name to something else (Move_1).

Since every change to one re-hashes and re-checks them all, saving says when it makes a cycle, or makes one bigger, that's worth avoiding (it saves all the same):

  • one only types make: warning: lib.Shop, lib.describeShop became one cycle (2 definitions) … It's a cycle only because types name each other (lib.describeShop's types name lib.Shop). A function taking the class that calls it, say; typing it with an interface of what it needs, rather than the class, breaks the cycle;
  • a big one of code calling itself (five definitions or more): splitting it where you can keeps changes small.

Two or three functions that recurse together aren't mentioned, and neither is a cycle saved again as big as it was.

7.5 Names

Names are labels; changing them never touches code.

  • tcm mv lib.parse lib.parseConfig renames a definition, or a whole namespace (tcm mv lib util).
  • tcm alias lib.parse lib.read gives a definition a second name.
  • tcm rm lib.parse removes a name. If it's the last name of something other definitions use, it refuses unless --force; those definitions keep working either way.

7.6 Undo

tcm history lists this branch's history, newest first: one entry per command, with what it changed. tcm reset 1 undoes the last command (any entry works: a number of entries back, or its hash), as a new entry, so a reset can always be reset again. tcm diff shows what the last command changed, or what changed since an entry: tcm diff 3. See history.

7.7 Browsing

  • tcm ls [namespace] lists names, with hashes and types.
  • tcm view <name> shows a definition with current names, and where it runs.
  • tcm find <text> searches names; tcm find ':(xs: T[]) => T' searches by type (finding code).
  • tcm deps <name> and tcm dependents <name> show what it uses and what uses it.
  • tcm docs <name> shows its documentation (docs).

In VS Code, all of this is in the sidebar and in hovers (editors).

built from c482a14e