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
@@ -0,0 +1,26 @@
# Project guidance

A core objective of the library is type safety and ease of use of the user-facing API, for both humans and agents.

The goal is eventually to merge this inside the core of the `effect` library, so plan changes according to the patterns and expectations of `effect`.

Make architectural decisions for the long term. Do not accept a stopgap that only works for now and is meant to be replaced later.

## Feature verification workflow

Before implementing a feature, run:

```sh
pnpm perf:types
```

Record the type-performance results as the baseline for the feature.

After implementing the feature, run:

```sh
pnpm typecheck
pnpm perf:types
```

Compare the final type-performance results with the baseline. When reporting the completed work, include the before and after results and call out the additional type-instantiation cost of the feature, including regressions or improvements.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"test": "vitest run",
"test:types": "tstyche",
"typecheck": "tsc -p tsconfig.json --noEmit",
"perf:types": "pnpm build && node scripts/type-performance.mjs",
"format": "prettier --write .",
"format:check": "prettier --check .",
"test:consumer": "node scripts/test-consumer.mjs",
Expand Down
12 changes: 12 additions & 0 deletions perf/types/define-states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

const States = Machine.defineStates(State.cases)

void States
9 changes: 9 additions & 0 deletions perf/types/effect-only.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

void State
35 changes: 35 additions & 0 deletions perf/types/handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

const Event = Schema.TaggedUnion({
Start: {},
Finish: { value: Schema.String }
})

const States = Machine.defineStates(State.cases)

const machine = Machine.make({
states: States.states,
events: [Event.cases.Start, Event.cases.Finish],
initial: () => States.initial.Idle(State.cases.Idle.make({}))
}).handle({
Idle: {
on: {
Start: ({ target }) => target.full.Running(State.cases.Running.make({}))
}
},
Running: {
on: {
Finish: ({ event, target }) => target.full.Done(State.cases.Done.make({ value: event.value }))
}
},
Done: {}
})

void machine
11 changes: 11 additions & 0 deletions perf/types/import-only.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

void Machine
void State
18 changes: 18 additions & 0 deletions perf/types/make-control.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

const Event = Schema.TaggedUnion({
Start: {},
Finish: { value: Schema.String }
})

const States = Machine.defineStates(State.cases)

void Event
void States
23 changes: 23 additions & 0 deletions perf/types/make.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Machine } from "@typeonce/effect-machine"
import { Schema } from "effect"

const State = Schema.TaggedUnion({
Idle: {},
Running: {},
Done: { value: Schema.String }
})

const Event = Schema.TaggedUnion({
Start: {},
Finish: { value: Schema.String }
})

const States = Machine.defineStates(State.cases)

const machine = Machine.make({
states: States.states,
events: [Event.cases.Start, Event.cases.Finish],
initial: () => States.initial.Idle(State.cases.Idle.make({}))
})

void machine
156 changes: 156 additions & 0 deletions scripts/type-performance.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { spawnSync } from "node:child_process"
import { resolve } from "node:path"

const root = resolve(import.meta.dirname, "..")
const tsc = resolve(root, "node_modules", "typescript", "bin", "tsc")

const scenarios = [
{
id: "effect-only",
label: "Effect only",
file: "effect-only.ts"
},
{
id: "import-only",
label: "Import effect-machine",
file: "import-only.ts",
control: "effect-only"
},
{
id: "define-states",
label: "Machine.defineStates (3 states)",
file: "define-states.ts",
control: "import-only"
},
{
id: "make-control",
label: "Machine.make setup",
file: "make-control.ts",
hidden: true
},
{
id: "make",
label: "Machine.make (3 states, 2 events)",
file: "make.ts",
control: "make-control"
},
{
id: "handle",
label: "machine.handle (3 states, 2 transitions)",
file: "handle.ts",
control: "make"
}
]

const compilerArguments = [
"--ignoreConfig",
"--noEmit",
"--incremental",
"false",
"--strict",
"--skipLibCheck",
"true",
"--target",
"ES2022",
"--module",
"NodeNext",
"--moduleResolution",
"NodeNext",
"--verbatimModuleSyntax",
"true",
"--exactOptionalPropertyTypes",
"true",
"--lib",
"ES2022",
"--pretty",
"false",
"--extendedDiagnostics"
]

const run = (args) => {
const result = spawnSync(process.execPath, [tsc, ...args], {
cwd: root,
encoding: "utf8"
})

if (result.status !== 0) {
throw new Error([result.stdout?.trim(), result.stderr?.trim()].filter(Boolean).join("\n"))
}

return result.stdout
}

const readMetric = (output, name) => {
const match = output.match(new RegExp(`^${name}:\\s+([0-9.]+)`, "m"))
if (match === null) {
throw new Error(`TypeScript did not report the ${name} metric`)
}
return Number(match[1])
}

const version = run(["--version"])
.trim()
.replace(/^Version\s+/, "")
const results = new Map()

for (const scenario of scenarios) {
const output = run([...compilerArguments, resolve(root, "perf", "types", scenario.file)])

results.set(scenario.id, {
instantiations: readMetric(output, "Instantiations"),
checkTime: readMetric(output, "Check time")
})
}

const visibleScenarios = scenarios.filter((scenario) => scenario.hidden !== true)
const rows = visibleScenarios.map((scenario) => {
const result = results.get(scenario.id)
const control = scenario.control === undefined ? undefined : results.get(scenario.control)
const delta = control === undefined ? undefined : result.instantiations - control.instantiations

return {
scenario: scenario.label,
instantiations: result.instantiations.toLocaleString("en-US"),
delta: delta === undefined ? "baseline" : `${delta >= 0 ? "+" : ""}${delta.toLocaleString("en-US")}`,
checkTime: `${result.checkTime.toFixed(2)}s`
}
})

const widths = {
scenario: Math.max("Scenario".length, ...rows.map((row) => row.scenario.length)),
instantiations: Math.max("Instantiations".length, ...rows.map((row) => row.instantiations.length)),
delta: Math.max("Marginal".length, ...rows.map((row) => row.delta.length)),
checkTime: Math.max("Check time".length, ...rows.map((row) => row.checkTime.length))
}

const formatRow = (row) =>
[
row.scenario.padEnd(widths.scenario),
row.instantiations.padStart(widths.instantiations),
row.delta.padStart(widths.delta),
row.checkTime.padStart(widths.checkTime)
].join(" ")

console.log(`Type performance (TypeScript ${version}, skipLibCheck=true)\n`)
console.log(
formatRow({
scenario: "Scenario",
instantiations: "Instantiations",
delta: "Marginal",
checkTime: "Check time"
})
)
console.log(
formatRow({
scenario: "-".repeat(widths.scenario),
instantiations: "-".repeat(widths.instantiations),
delta: "-".repeat(widths.delta),
checkTime: "-".repeat(widths.checkTime)
})
)
for (const row of rows) {
console.log(formatRow(row))
}

console.log("\nMarginal is measured against the matching setup without that API call.")
console.log("Check time is informational; instantiations are the stable comparison metric.")