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.utilas the first line puts the file's definitions underlib.util. Without it they go at the root.tcm add --into lib.utildoes the same from the command line. - Using the codebase.
import { square } from "#codebase"imports from the root namespace;#codebase/lib/utilfromlib.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 --freshruns them all again. - What goes in. Top-level functions,
consts, classes, types and interfaces each become a definition.exportis optional and makes no difference to tcm. A top-levelletis 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-levelvarand 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):
| status | meaning | to save it |
|---|---|---|
new | not in the codebase yet | tcm add |
identical | already saved, exactly this | nothing to do |
update | the name exists, with different code | tcm update |
doc | same code, a new doc comment | tcm add or tcm update |
alias | this exact code is already saved under another name | tcm add gives it a second name |
resolves | the name is conflicted (after a merge): saving this resolves it to this definition | tcm 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.parseConfigrenames a definition, or a whole namespace (tcm mv lib util).tcm alias lib.parse lib.readgives a definition a second name.tcm rm lib.parseremoves 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>andtcm 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).