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
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,9 @@
{
"run": "git add -A && git diff --cached --exit-code"
},
{
"run": "! grep -rnE '^(/\\*\\*.*@typedef|\\s\\* *@typedef)' --include='*.mjs' --exclude-dir=node_modules ."
},
{
"run": "npx tsc"
},
Expand Down
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ Every new `.f.mjs` module ships a co-located `proof.f.mjs` with **100% proof
coverage** — every export called, every line executed, every branch taken.
Values are immutable (no in-place mutation, no `.push`/`Map#set`/index
assignment), there is no `try`/`catch` and no regular expressions, and types are
written in JSDoc with a sibling `types.ts` for a type-level API.
written in JSDoc with a sibling `types.ts` for a type-level API. No authored
`.mjs` anywhere in the repository — `fjs/` or not — may contain a **file-scope**
JSDoc `@typedef`; function-local typedefs are allowed. Named types live in
`types.ts` (the public declaration closure) or an optional `private.ts`.

Testing, documentation, and the full coding style: [fjs/AGENTS.md](./fjs/AGENTS.md).

Expand Down
5 changes: 5 additions & 0 deletions changelog/unreleased/1750.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
- **BREAKING CHANGES:** type import paths changed: `Grammar` →
`fjs/fsm/types.ts`; `MemoryOperationMap`, `MemoryRun`, `Uuid` →
`fjs/effects/node/memory/types.ts`; `BrowserTestReport` →
`fjs/emergent_testing/types.ts`; the JSON-Schema `Unknown` alias is gone —
spell it `Ts<typeof unknown>`.
62 changes: 47 additions & 15 deletions fjs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -247,15 +247,42 @@ changes. A separately useful type-level API may live in an authored sibling
`types.ts`; that file remains TypeScript type source and holds no runtime
implementation.

Name implementation-only JSDoc typedefs with a leading `_`
(`/** @typedef {number} _Type */`). Declaration emit cannot strip them yet, so
the underscore — not the emitted `.d.ts` — is what marks a name private,
and renaming or removing a `_`-prefixed alias is not by itself a breaking
change. The public contract still governs transitive effects. See
[Private JSDoc typedefs](./fsc/README.md#private-jsdoc-typedefs) for the
full rule and examples.

Use `@typedef` for a named type and `@template` for its type parameters. A
No authored `.mjs` may contain a **file-scope** JSDoc `@typedef` — anywhere in
the repository, whatever the directory or basename. Function-local typedefs are
allowed, and are the normal home for compile-time proof types (see the
`consistency` and `signatures` entries in `fjs/edag/proof.f.mjs` and
`fjs/effects/proof.f.mjs`). A named file-scope type goes to one of:

- the sibling `types.ts` when it is part of the **public declaration closure** —
public types, plus any private `_` helper a shipped public declaration
reaches transitively (e.g. `_Byte` in `fjs/types/byte_set/types.ts`) — or the
type is inlined into the annotation instead;
- an optional sibling `private.ts` for implementation-private types outside the
public closure, when separating them reads cleaner than inlining (e.g.
`fjs/common/monoid/private.ts`, `fjs/rtti/data/private.ts`); do not create it
mechanically for every `_` name;
- nowhere: a short type used once or twice is simply inlined.

Name private types and private runtime constants with a leading `_`, even when
module linkage requires an export: exportability is linkage, not API status, so
renaming or removing a `_`-prefixed name is not by itself a breaking change.
The public contract still governs transitive effects. See
[Private types](./fsc/README.md#private-types) for the full rule.

The intra-directory dependency direction is
`types.ts <- private.ts <- module.f.mjs <- proof.f.mjs <- module.mjs <- proof.mjs`
(dependency to dependent; a layering guide, not a requirement that every file
exists). `types.ts` must not depend on `private.ts`, and verification moves
downstream: an assertion that checks the implementation belongs in a proof
function, not in `types.ts`. Recursive RTTI whose annotation needs a named
public type may stay in `module.f.mjs` (e.g. `exp` in `fjs/edag/module.f.mjs`),
and declarative compile-time/runtime constants shared between TypeScript and
runtime code may be split into a normal subordinate metaprogramming module such
as `meta/module.f.mjs` when that helps — it is an ordinary module, discovered
and covered like any other `module.f.mjs`, never a requirement.

Use `@typedef` (function-local in `.mjs`, or `export type` in `types.ts` /
`private.ts`) for a named type and `@template` for its type parameters. A
constraint goes in braces before the parameter name:

```js
Expand Down Expand Up @@ -435,10 +462,15 @@ inference with an `Assert<Equal<…>>` in the proof, per
literal, since a primitive would pass with or without it:

```js
const v = validate({ a: 42, b: 'hello' })
/** @typedef {Assert<Equal<typeof v, Validate<{ readonly a: 42, readonly b: 'hello' }>>>} _ConstParameter */
constParameter: () => {
const v = validate({ a: 42, b: 'hello' })
/** @typedef {Assert<Equal<typeof v, Validate<{ readonly a: 42, readonly b: 'hello' }>>>} _ConstParameter */
},
```

The typedef sits inside the proof entry because an authored `.mjs` carries no
file-scope typedef (§3.2).

#### Avoid `as` type assertions

Avoid `as` type assertions (except `as const`). Treat them like `unsafe` in Rust
Expand Down Expand Up @@ -536,10 +568,10 @@ that context on its own: `ToAsyncOperationMap<O>` is a mapped type keyed on
back out of the argument. Left to argument inference `O` falls back to its
`Operation` constraint — payloads and outputs `never` — which no real map is
assignable to, and the call site reaches for exactly the cast this section warns
about. **Annotate the result instead**: pin the runner's own type
(`/** @type {_EffectToPromise} */`, `/** @type {MemoryRun} */`) and `O` is
inferred from the return type, giving the call a real `O` to check its argument
against. Both Node runners are written that way —
about. **Annotate the result instead**: pin the runner's own type — an inline
generic annotation, or a `types.ts` name such as `/** @type {MemoryRun} */`
and `O` is inferred from the return type, giving the call a real `O` to check
its argument against. Both Node runners are written that way —
`fjs/effects/node/module.mjs`'s `runNodeEffect` and
`fjs/effects/node/memory/module.mjs`'s `memoryRun`.

Expand Down
29 changes: 2 additions & 27 deletions fjs/asn.1/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*
* @import { Unpacked, Vec } from '../types/bit_vec/types.ts'
* @import { ObjectIdentifier, Raw, Record, Sequence, SupportedRecord, _Tag } from './types.ts'
* @import { _ClassPc, _ParsedTag } from './private.ts'
*/

import { bitLength, divUp8 } from '../types/bigint/module.f.mjs'
Expand All @@ -32,29 +33,10 @@ const pop8 = pop(8n)

// tag

/**
* @typedef {|
* 0b000_00000n |
* 0b001_00000n |
* 0b010_00000n |
* 0b011_00000n |
* 0b100_00000n |
* 0b101_00000n |
* 0b110_00000n |
* 0b111_00000n
* } _ClassPc
*/

const classPcMask = 0b111_00000n

const tagNumberMask = 0b000_11111n

/**
* Note: the tag number (the second parameter) can be arbitrarily large,
* so we can't just use a single byte to represent it.
* @typedef {readonly[_ClassPc, bigint]} _ParsedTag
*/

/** @type {([classPc, number]: _ParsedTag) => Vec} */
const parsedTagEncode = ([classPc, number]) => {
const [firstByteNumber, rest] = number < tagNumberMask
Expand Down Expand Up @@ -140,14 +122,7 @@ export const constructedSet = 0x31n // constructed | set

//

/**
* @typedef {{
* readonly byteLen: bigint
* readonly v: Vec
* }} _Round8
*/

/** @type {(_: Unpacked) => _Round8} */
/** @type {(_: Unpacked) => { readonly byteLen: bigint, readonly v: Vec }} */
const round8 = ({ length, uint }) => {
const byteLen = divUp8(length)
return { byteLen, v: vec(byteLen << 3n)(uint) }
Expand Down
20 changes: 20 additions & 0 deletions fjs/asn.1/private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Implementation-private types for ASN.1 tag encoding.
*/

/** The top three bits of a tag's first byte: class and constructed flag. */
export type _ClassPc =
| 0b000_00000n
| 0b001_00000n
| 0b010_00000n
| 0b011_00000n
| 0b100_00000n
| 0b101_00000n
| 0b110_00000n
| 0b111_00000n

/**
* Note: the tag number (the second element) can be arbitrarily large,
* so we can't just use a single byte to represent it.
*/
export type _ParsedTag = readonly [_ClassPc, bigint]
10 changes: 2 additions & 8 deletions fjs/bnf/data/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@
* @module
*
* @import { DataRule, Rule as FRule, Sequence as FSequence } from '../types.ts'
* @import { StringMap } from '../../types/object/types.ts'
* @import { StringSet } from '../../types/string_set/types.ts'
* @import { EmptyTag, Repeat, Rule, RuleSet, Sequence, Variant } from './types.ts'
* @import { EmptyTag, Repeat, Rule, RuleSet, Sequence, Variant, _EmptyTagMap } from './types.ts'
* @import { _FRuleMap, _NewRule } from './private.ts'
*/

import { stringToCodePointList } from '../../text/utf16/module.f.mjs'
Expand All @@ -37,8 +37,6 @@ import { contains, set } from '../../types/string_set/module.f.mjs'
*/
export const isRepeat = rule => typeof rule === 'string'

/** @typedef {StringMap<EmptyTag>} _EmptyTagMap */

/** @type {(map: _EmptyTagMap) => (rule: Rule) => EmptyTag} */
const emptyTagOf = map => rule => {
if (typeof rule === 'number') {
Expand Down Expand Up @@ -102,8 +100,6 @@ export const emptyTagMap = ruleSet => {

//

/** @typedef {StringMap<FRule>} _FRuleMap */

const { entries } = Object

/** @type {(map: _FRuleMap) => (fr: FRule) => string | undefined} */
Expand All @@ -127,8 +123,6 @@ const newName = (map, name) => {
return result
}

/** @typedef {(m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule]} _NewRule */

/** @type {(list: FSequence) => _NewRule} */
const sequence = list => map => {
/** @type {Sequence} */
Expand Down
19 changes: 19 additions & 0 deletions fjs/bnf/data/private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Implementation-private types for the `toData` conversion.
*/

import type { Rule as FRule } from '../types.ts'
import type { StringMap } from '../../types/object/types.ts'
import type { Rule, RuleSet } from './types.ts'

/**
* Functional rules already converted, keyed by the generated rule name — the
* memo that keeps a shared functional rule one named data rule.
*/
export type _FRuleMap = StringMap<FRule>

/**
* One conversion step: given the memo so far, produces the extended memo, the
* rules the step generated, and the converted rule itself.
*/
export type _NewRule = (m: _FRuleMap) => readonly [_FRuleMap, RuleSet, Rule]
5 changes: 5 additions & 0 deletions fjs/bnf/data/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,8 @@ export type RuleSet = Readonly<Record<string, Rule>>
* variant branch.
*/
export type EmptyTag = string | true | undefined

/**
* The {@link EmptyTag} of every rule in a {@link RuleSet}, keyed by rule name.
*/
export type _EmptyTagMap = StringMap<EmptyTag>
24 changes: 2 additions & 22 deletions fjs/bnf/descent/module.f.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@
* @import { Rule as DataRule, RuleSet, Sequence } from '../data/types.ts'
* @import { Rule as FRule } from '../types.ts'
* @import { List } from '../../types/list/types.ts'
* @import { Ast, AstResult, AstSequence, AstTag, Cursor } from '../matcher/types.ts'
* @import { Ast, AstSequence, AstTag, Cursor } from '../matcher/types.ts'
* @import { CodePointMeta, DescentFailure, DescentMatch, DescentMatchResult, DescentMatchRule } from './types.ts'
* @import { _Failure, _Result } from './private.ts'
*/

import { rangeDecode } from '../module.f.mjs'
Expand All @@ -40,27 +41,6 @@ import { definedEntries } from '../../types/object/module.f.mjs'
import { emptyTagMap, isRepeat, toData } from '../data/module.f.mjs'
import { leafAt, mrFail, mrSuccess, physicalIdx, symbolAt } from '../matcher/module.f.mjs'

/**
* The furthest-failure record while matching, positioned by the complete
* {@link Cursor}. {@link DescentFailure} is its public, physically-positioned
* form.
*
* @typedef {{
* readonly pos: Cursor
* readonly expected: readonly TerminalRange[]
* }} _Failure
*/

/**
* The machine's own result: a {@link DescentMatchResult} positioned by the
* complete cursor, and with no failure record — that one is tracked per match
* rather than per frame. This backend always has a position, so it needs no
* `null` case.
*
* @template T
* @typedef {AstResult<CodePointMeta<T>, Cursor>} _Result
*/

/**
* A leaf here is a code point with its metadata, so its symbol is the first
* half. This is the only thing {@link symbolAt} needs to know about a leaf.
Expand Down
25 changes: 25 additions & 0 deletions fjs/bnf/descent/private.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Implementation-private types for the recursive descent matcher backend.
*/

import type { TerminalRange } from '../types.ts'
import type { AstResult, Cursor } from '../matcher/types.ts'
import type { CodePointMeta, DescentFailure } from './types.ts'

/**
* The furthest-failure record while matching, positioned by the complete
* {@link Cursor}. {@link DescentFailure} is its public, physically-positioned
* form.
*/
export type _Failure = {
readonly pos: Cursor
readonly expected: readonly TerminalRange[]
}

/**
* The machine's own result: a `DescentMatchResult` positioned by the complete
* cursor, and with no failure record — that one is tracked per match rather
* than per frame. This backend always has a position, so it needs no `null`
* case.
*/
export type _Result<T> = AstResult<CodePointMeta<T>, Cursor>
Loading
Loading