Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,32 @@ Inline `@type` casts carried over from `as` assertions during the
TypeScript-to-JavaScript migration still exist in the tree; converting one to the
declaration form is a welcome cleanup wherever the rewrite is straightforward.

When the value being narrowed is an invariant a comment would otherwise have to
assert on trust — "this is never `undefined`/`null` because the caller already
guaranteed X" — prefer `assert`/`assertNotNullish` from
[`fjs/asserts/module.f.mjs`](./fjs/asserts/module.f.mjs) over a cast:

```js
const refCounter = assertNotNullish(refs.get(entry))
```

rather than

```js
const refCounter = /** @type {_RefCounter} */ (refs.get(entry))
```

A cast is a claim the compiler takes on faith and erases at runtime: if the
invariant it documents ever breaks — a future edit to the code it depends on,
a case the original reasoning missed — the narrowed value is silently wrong
instead of the assertion failing where the break actually happened.
`assert`/`assertNotNullish` narrow exactly the same way (via `asserts v` /
a checked return type) but also check the claim every time, so a broken
invariant throws immediately at the point that assumed it, not later at
some unrelated crash site. Reach for a cast only when there is truly no
runtime check to perform — e.g. `@type {const}` below, or narrowing across a
boundary the type system cannot express at all.

`@type {const}` (the JSDoc equivalent of `as const`, see "Pin literal
`const`s" above) is the one case where this preference inverts: it **must**
stay an inline cast on the expression —
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ history.

## Unreleased

- `djs/serializer`: `stringify`'s `constSerialize` drops a defensive throw for
a `refs` lookup that can never miss — `consts` only ever holds values
`getConstants` already found an entry for
[#1544](https://github.com/functionalscript/functionalscript/pull/1544)
- `types/uint8array`: `toVec` attempts the conversion instead of precomputing a
byte-count bound; behavior and error message unchanged
[#1543](https://github.com/functionalscript/functionalscript/pull/1543)
Expand Down
9 changes: 5 additions & 4 deletions fjs/djs/serializer/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { entries } = Object
import { compose, fn } from '../../types/function/module.f.mjs'
import { serialize as bigintSerialize } from '../../types/bigint/module.f.mjs'
import { objectWrap, arrayWrap, stringSerialize, numberSerialize, nullSerialize, boolSerialize } from '../../media/json/serializer/module.f.mjs'
import { assertNotNullish } from '../../asserts/module.f.mjs'

const colon = [':']

Expand Down Expand Up @@ -183,12 +184,12 @@ const addRef = djs => refs => {
export const stringify = sort => djs => {
const refs = countRefs(djs)
const consts = getConstants(refs)(djs)
// `consts` only ever holds values `getConstants` found `shared` for, i.e.
// values with an entry already in `refs` — so `refs.get(entry)` here is
// always defined.
/** @type {(entry: Unknown) => List<string>} */
const constSerialize = entry => {
const refCounter = refs.get(entry)
if (refCounter === undefined) {
throw 'unexpected behavior'
}
const refCounter = assertNotNullish(refs.get(entry))
return flat([['const c'], numberSerialize(refCounter[0]), [' = '], serializeWithConst(sort)(refs)(entry)(entry), ['\n']])
}
const constStrings = flatMap(constSerialize)(consts)
Expand Down
Loading