@abide/abide 0.105.0 → 0.106.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/bin/abide.ts +4 -1
- package/docs/REGISTRY.md +156 -44
- package/docs/RULEBOOK.md +375 -87
- package/docs/abide.md +7243 -10256
- package/package.json +4 -1
- package/scaffold/AGENTS.md +2 -0
- package/scaffold/src/getGreeting.ts +6 -0
- package/scaffold/src/index.abide +3 -1
- package/scaffold/src/tests/getGreeting.test.ts +6 -0
- package/scaffold/src/tests/index.test.ts +18 -0
- package/src/compiler/DOCS_SITE.ts +2 -0
- package/src/compiler/Lowering.ts +234 -14
- package/src/compiler/PositionTable.ts +14 -7
- package/src/compiler/PositionedRefusal.ts +13 -0
- package/src/compiler/Refusals.ts +18 -11
- package/src/compiler/SHARED_NAMES.ts +2 -0
- package/src/compiler/ShadowMap.ts +37 -0
- package/src/compiler/TEMPLATE_BLOCKS.ts +12 -0
- package/src/compiler/TypeSession.ts +410 -31
- package/src/compiler/appLayout.ts +18 -2
- package/src/compiler/argsSchemaOf.ts +55 -9
- package/src/compiler/checkApp.ts +33 -0
- package/src/compiler/checkedShadow.ts +36 -22
- package/src/compiler/citedClauses.ts +14 -0
- package/src/compiler/classifyRead.ts +6 -0
- package/src/compiler/classifyTemplate.ts +2 -1
- package/src/compiler/compileApp.ts +2 -2
- package/src/compiler/declaredExports.ts +27 -6
- package/src/compiler/emitClient.ts +304 -103
- package/src/compiler/handlersUnder.ts +4 -0
- package/src/compiler/invocationProps.ts +1 -1
- package/src/compiler/isIgnored.ts +27 -0
- package/src/compiler/parseAbide.ts +176 -76
- package/src/compiler/projectShadow.ts +30 -6
- package/src/compiler/readSources.ts +1 -1
- package/src/compiler/readsIn.ts +100 -1
- package/src/compiler/renderMarkup.ts +36 -5
- package/src/compiler/scaffold.ts +19 -6
- package/src/compiler/serveLanguage.ts +156 -21
- package/src/compiler/templateContext.ts +150 -0
- package/src/compiler/tokenize.ts +6 -0
- package/src/server/BRIDGE_VERSION.ts +1 -1
- package/src/server/Inbox.ts +8 -0
- package/src/server/MCP_PROTOCOL_VERSION.ts +1 -1
- package/src/server/MOUNTABLE.ts +6 -0
- package/src/server/abideModules.ts +8 -1
- package/src/server/answerRequest.ts +3 -1
- package/src/server/answerRpc.ts +26 -12
- package/src/server/answerSocket.ts +26 -1
- package/src/server/argsFor.ts +4 -3
- package/src/server/assistant.ts +19 -13
- package/src/server/bridgeRelay.ts +108 -20
- package/src/server/claudeApi.ts +7 -7
- package/src/server/claudeCode.ts +11 -11
- package/src/server/commands.ts +73 -9
- package/src/server/createApp.ts +41 -7
- package/src/server/devWorker.ts +3 -3
- package/src/server/generatedSurfaces.ts +37 -4
- package/src/server/muxUpgrade.ts +47 -0
- package/src/server/preload.ts +13 -2
- package/src/server/render.ts +4 -1
- package/src/server/runCompiled.ts +1 -0
- package/src/server/seedBuffer.ts +31 -1
- package/src/server/serveDev.ts +83 -72
- package/src/server/surfaceOf.ts +3 -0
- package/src/server/testMount.ts +53 -0
- package/src/shared/ADDRESSES.ts +1 -1
- package/src/shared/ERROR_META.ts +3 -0
- package/src/shared/Schema.ts +1 -1
- package/src/shared/WORK.ts +14 -12
- package/src/shared/applyPatch.ts +1 -1
- package/src/shared/boundValue.ts +81 -0
- package/src/shared/index.ts +7 -2
- package/src/shared/jsonSchema.ts +25 -2
- package/src/shared/log.ts +11 -1
- package/src/shared/reactive/Keyed.ts +30 -6
- package/src/shared/reactive/PATCHES.ts +1 -1
- package/src/shared/reactive/Reactive.ts +118 -60
- package/src/shared/reactive/canonicalKey.ts +1 -1
- package/src/shared/reactive/index.ts +13 -5
- package/src/shared/reactive/recordWrites.ts +1 -1
- package/src/shared/reactive/watch.ts +26 -4
- package/src/shared/rpc.ts +6 -1
- package/src/shared/url.ts +7 -3
- package/src/shared/validateJson.ts +1 -1
- package/src/ui/ArmRegion.ts +2 -2
- package/src/ui/List.ts +9 -9
- package/src/ui/RawRun.ts +1 -1
- package/src/ui/Router.ts +194 -36
- package/src/ui/Scope.ts +29 -0
- package/src/ui/adoptWalk.ts +3 -3
- package/src/ui/applyHead.ts +25 -3
- package/src/ui/bindProperty.ts +70 -22
- package/src/ui/index.ts +1 -1
- package/src/ui/leave.ts +5 -0
- package/src/ui/listenForRoutes.ts +14 -3
- package/src/ui/reconcile.ts +48 -28
- package/src/ui/regions.ts +36 -4
- package/src/ui/rows.ts +108 -28
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abide/abide",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.106.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
|
|
6
6
|
"license": "MIT",
|
|
@@ -62,6 +62,9 @@
|
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"typescript": "^7.0.2"
|
|
64
64
|
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@abide/harness": "workspace:*"
|
|
67
|
+
},
|
|
65
68
|
"scripts": {
|
|
66
69
|
"typecheck": "bun check",
|
|
67
70
|
"prepack": "bun ../../scripts/packDocs.ts"
|
package/scaffold/AGENTS.md
CHANGED
|
@@ -17,6 +17,8 @@ before writing code, rather than guessing from Svelte, Vue or React. It installs
|
|
|
17
17
|
TypeScript service do not read `.abide` files, so a clean `tsc` says nothing about them. Run
|
|
18
18
|
`bun run check` after every change, and treat its warnings as bugs: several of them name a read
|
|
19
19
|
that will not update when its value changes.
|
|
20
|
+
- `bun run test` — runs `bun test`. `src/tests/getGreeting.test.ts` is the pattern: a test imports a
|
|
21
|
+
handler or a `.abide` file and calls it.
|
|
20
22
|
- `bun run dev` — serves the app on a local port and rebuilds when a file changes.
|
|
21
23
|
- `bun run build` then `bun run start` — the production build, and the app served from it.
|
|
22
24
|
|
package/scaffold/src/index.abide
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
import { reactive } from '@abide/abide'
|
|
3
|
+
import getGreeting from '#src/getGreeting.ts'
|
|
3
4
|
|
|
4
5
|
let count = reactive(0)
|
|
6
|
+
const greeting = reactive(() => getGreeting({ name: 'world' }))
|
|
5
7
|
</script>
|
|
6
8
|
|
|
7
9
|
<head><title>Hello</title></head>
|
|
8
10
|
|
|
9
11
|
<main>
|
|
10
|
-
<h1>
|
|
12
|
+
<h1>{greeting}</h1>
|
|
11
13
|
<p>Clicked {count} times.</p>
|
|
12
14
|
<button onclick={() => { count = count + 1 }}>Click</button>
|
|
13
15
|
</main>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { expect, test } from 'bun:test'
|
|
2
|
+
import { flushSync } from '@abide/abide'
|
|
3
|
+
import { measure, mount } from '@abide/harness/abide'
|
|
4
|
+
import Index from '../index.abide'
|
|
5
|
+
|
|
6
|
+
test('a click rewrites the count and builds nothing', async () => {
|
|
7
|
+
using _page = await mount(Index({}))
|
|
8
|
+
const work = measure(() => {
|
|
9
|
+
document.querySelector('button')?.click()
|
|
10
|
+
flushSync()
|
|
11
|
+
})
|
|
12
|
+
expect(document.querySelector('p')?.textContent).toBe('Clicked 1 times.')
|
|
13
|
+
// One text node written, one effect woken, and no node made or moved.
|
|
14
|
+
expect(work.dataWrites).toBe(1)
|
|
15
|
+
expect(work.wakes).toBe(1)
|
|
16
|
+
expect(work.nodesCreated).toBe(0)
|
|
17
|
+
expect(work.elementsMoved).toBe(0)
|
|
18
|
+
})
|
package/src/compiler/Lowering.ts
CHANGED
|
@@ -33,6 +33,20 @@ import { nesting, type Token, tokenize } from './tokenize.ts'
|
|
|
33
33
|
// no parentheses to survive one.
|
|
34
34
|
const SHORT_CIRCUIT: ReadonlySet<string> = new Set(['||', '&&', '??'])
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* 32.67 — how a `{#for}` row's item is read where it is being emitted.
|
|
38
|
+
*
|
|
39
|
+
* `own` is the row's own top level, whose slots reading the item and nothing that moves are written
|
|
40
|
+
* by the row's `make` and `update` and bind nothing; a binding there is an item watch, the one
|
|
41
|
+
* writer of its slot, reading the item through `itemOf`.
|
|
42
|
+
* `nested` is inside a region of the row, a branch-local `<script>` or anything else the `update`
|
|
43
|
+
* does not reach, where every read but a listener's is a read of the cell. Where an item has no mode
|
|
44
|
+
* it is read as the value.
|
|
45
|
+
*/
|
|
46
|
+
type ItemMode = 'own' | 'nested'
|
|
47
|
+
/** 32.67 — how many reads of a row's item were emitted as a cell call and as a record read. */
|
|
48
|
+
export type ItemUses = { cell: number; record: number }
|
|
49
|
+
|
|
36
50
|
/** 23.9 — a page's or a layout's route, which its `route.params` promises. */
|
|
37
51
|
export type Routed = { route: string; kind: 'page' | 'layout' }
|
|
38
52
|
|
|
@@ -93,6 +107,20 @@ export class Lowering {
|
|
|
93
107
|
* what its `route.params` is typed by.
|
|
94
108
|
*/
|
|
95
109
|
private readonly routed: Routed | undefined
|
|
110
|
+
/**
|
|
111
|
+
* 32.67 — per `{#for}` item (by where its binding starts), how a read of it is emitted now, the
|
|
112
|
+
* name its row's record is held under, and how it was read. Set by the client's emitter around
|
|
113
|
+
* a row's mount alone (`readItemsAs`).
|
|
114
|
+
*/
|
|
115
|
+
private readonly itemModes = new Map<
|
|
116
|
+
number,
|
|
117
|
+
{ mode: ItemMode; record: string; uses: ItemUses }
|
|
118
|
+
>()
|
|
119
|
+
/**
|
|
120
|
+
* What is being lowered that reads a row's item off its record: a listener, at any depth, or a
|
|
121
|
+
* binding of the row's own top level (`binding`); `null` for anything else.
|
|
122
|
+
*/
|
|
123
|
+
private recording: 'listener' | 'binding' | null = null
|
|
96
124
|
|
|
97
125
|
constructor(
|
|
98
126
|
file: AbideFile,
|
|
@@ -109,6 +137,110 @@ export class Lowering {
|
|
|
109
137
|
this.routed = routed
|
|
110
138
|
}
|
|
111
139
|
|
|
140
|
+
/**
|
|
141
|
+
* 32.67 — where a `{#for}` block's item binding starts, the key every item question is asked by.
|
|
142
|
+
*/
|
|
143
|
+
private itemOf(block: object): number {
|
|
144
|
+
return this.projection.items.get(block) ?? -1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 32.67 — emits `body` with `block`'s item read in `mode`, its record held as `record` for a
|
|
149
|
+
* listener, and answers how the item was read: as a cell, or off the record.
|
|
150
|
+
*/
|
|
151
|
+
readItemsAs(
|
|
152
|
+
block: object,
|
|
153
|
+
mode: ItemMode,
|
|
154
|
+
record: string,
|
|
155
|
+
body: () => void,
|
|
156
|
+
): ItemUses {
|
|
157
|
+
const item = this.itemOf(block)
|
|
158
|
+
const uses = { cell: 0, record: 0 }
|
|
159
|
+
this.itemModes.set(item, { mode, record, uses })
|
|
160
|
+
try {
|
|
161
|
+
body()
|
|
162
|
+
} finally {
|
|
163
|
+
this.itemModes.delete(item)
|
|
164
|
+
}
|
|
165
|
+
return uses
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* 32.67 — emits `body` with every row item read at its own top level read as `nested`: what a
|
|
170
|
+
* region of the row binds is bound to the cell, since the row's `update` reaches only its own
|
|
171
|
+
* top level.
|
|
172
|
+
*/
|
|
173
|
+
nestItems(body: () => void): void {
|
|
174
|
+
const own: { mode: ItemMode }[] = []
|
|
175
|
+
for (const reading of this.itemModes.values())
|
|
176
|
+
if (reading.mode === 'own') own.push(reading)
|
|
177
|
+
for (const reading of own) reading.mode = 'nested'
|
|
178
|
+
try {
|
|
179
|
+
body()
|
|
180
|
+
} finally {
|
|
181
|
+
for (const reading of own) reading.mode = 'own'
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 32.67 — a listener, lowered: it runs on an event rather than in a binding, so a row's item is
|
|
187
|
+
* read off the row's record, which allocates nothing a row.
|
|
188
|
+
*/
|
|
189
|
+
listener(holder: object): string {
|
|
190
|
+
return this.recorded('listener', holder)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* 32.67 — a text or attribute binding's value, lowered: at a row's own top level it reads the
|
|
195
|
+
* item through `itemOf`, off the record until the row's first new item makes it a cell, so a row
|
|
196
|
+
* that never takes one allocates nothing for it.
|
|
197
|
+
*/
|
|
198
|
+
binding(holder: object): string {
|
|
199
|
+
return this.recorded('binding', holder)
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 32.67 — whether a binding reads a row's item at that row's own top level, which makes it one
|
|
204
|
+
* of the row's item watches (`Scope.itemWatch`).
|
|
205
|
+
*/
|
|
206
|
+
readsOwnItem(holder: object): boolean {
|
|
207
|
+
return this.holdsRead(
|
|
208
|
+
holder,
|
|
209
|
+
(read) =>
|
|
210
|
+
read.item !== -1 &&
|
|
211
|
+
this.itemModes.get(read.item)?.mode === 'own',
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private recorded(as: 'listener' | 'binding', holder: object): string {
|
|
216
|
+
const was = this.recording
|
|
217
|
+
this.recording = as
|
|
218
|
+
try {
|
|
219
|
+
return this.expression(holder)
|
|
220
|
+
} finally {
|
|
221
|
+
this.recording = was
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** 32.67 — whether a position reads `block`'s item. */
|
|
226
|
+
readsItem(holder: object, block: object): boolean {
|
|
227
|
+
const item = this.itemOf(block)
|
|
228
|
+
return this.holdsRead(holder, (read) => read.item === item)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* 32.67 — the names a row reads its item through, in the `{#for}`'s scope: its pattern's names,
|
|
233
|
+
* or the one name.
|
|
234
|
+
*/
|
|
235
|
+
itemNames(block: object): Set<string> {
|
|
236
|
+
const item = this.itemOf(block)
|
|
237
|
+
const names = new Set<string>()
|
|
238
|
+
for (const read of this.answer.reads)
|
|
239
|
+
if (read.item === item)
|
|
240
|
+
names.add(this.projection.text.slice(read.start, read.end))
|
|
241
|
+
return names
|
|
242
|
+
}
|
|
243
|
+
|
|
112
244
|
/**
|
|
113
245
|
* Whether this file is a component that takes a props record.
|
|
114
246
|
*
|
|
@@ -246,7 +378,12 @@ export class Lowering {
|
|
|
246
378
|
const to = span.start + text.trimEnd().length
|
|
247
379
|
const reads = this.answer.reads
|
|
248
380
|
const read = reads[this.answer.firstRead(from)]
|
|
249
|
-
if (
|
|
381
|
+
if (
|
|
382
|
+
read === undefined ||
|
|
383
|
+
read.from !== from ||
|
|
384
|
+
read.shape !== 'value' ||
|
|
385
|
+
read.item !== -1
|
|
386
|
+
)
|
|
250
387
|
return undefined
|
|
251
388
|
const path = source.slice(read.to, to)
|
|
252
389
|
if (!/^(\s*\.\s*[A-Za-z_$][\w$]*)+$/.test(path)) return undefined
|
|
@@ -285,7 +422,20 @@ export class Lowering {
|
|
|
285
422
|
* it NAMES a `Reactive`, which writes `{big()}` once where `big` reads `n` in its body.
|
|
286
423
|
*/
|
|
287
424
|
isLive(holder: object): boolean {
|
|
288
|
-
|
|
425
|
+
const span = this.spanOf(holder)
|
|
426
|
+
if (!this.answer.isStatic(span)) return true
|
|
427
|
+
// 32.67 — and inside a region of a row, a position reading the row's item is bound to its
|
|
428
|
+
// cell, which the proof sets aside: the server renders the row once.
|
|
429
|
+
return (
|
|
430
|
+
this.itemModes.size !== 0 &&
|
|
431
|
+
this.answer.holdsRead(span, (read) => this.isNested(read))
|
|
432
|
+
)
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private isNested(read: Read): boolean {
|
|
436
|
+
return (
|
|
437
|
+
read.item !== -1 && this.itemModes.get(read.item)?.mode === 'nested'
|
|
438
|
+
)
|
|
289
439
|
}
|
|
290
440
|
|
|
291
441
|
/**
|
|
@@ -323,7 +473,12 @@ export class Lowering {
|
|
|
323
473
|
holdsMarkup(run: readonly TemplateNode[]): boolean {
|
|
324
474
|
for (const piece of run) {
|
|
325
475
|
if (piece.kind !== 'expression') continue
|
|
326
|
-
|
|
476
|
+
// The PROOF, not `isLive`: a row's item makes a position live on the client alone, and
|
|
477
|
+
// both lanes have to agree on whether a run is anchored (32.30).
|
|
478
|
+
if (
|
|
479
|
+
!this.answer.isStatic(this.spanOf(piece)) ||
|
|
480
|
+
this.holdsRead(piece, isRawCallee)
|
|
481
|
+
)
|
|
327
482
|
return true
|
|
328
483
|
}
|
|
329
484
|
return false
|
|
@@ -395,6 +550,16 @@ export class Lowering {
|
|
|
395
550
|
const current = reads[read] as Read
|
|
396
551
|
if (current.start >= to) break
|
|
397
552
|
if (current.start < at) continue
|
|
553
|
+
// 32.67 — a row's item is the value wherever its row is not being mounted.
|
|
554
|
+
if (current.item !== -1) {
|
|
555
|
+
const rewritten = this.itemRead(current)
|
|
556
|
+
if (rewritten === undefined) continue
|
|
557
|
+
copy(current.from)
|
|
558
|
+
pin(current.from)
|
|
559
|
+
out += rewritten
|
|
560
|
+
at = current.to
|
|
561
|
+
continue
|
|
562
|
+
}
|
|
398
563
|
// 31.3 — the operand's own read is the one that does not lower — save that a written
|
|
399
564
|
// prop's name is its accessor, and the operand is the cell the accessor hands back.
|
|
400
565
|
if (holds && current.from === wholeFrom && current.to === wholeTo) {
|
|
@@ -431,6 +596,23 @@ export class Lowering {
|
|
|
431
596
|
at = current.to
|
|
432
597
|
continue
|
|
433
598
|
}
|
|
599
|
+
// 38.116 — THE CHECKER'S `props` CALL IS COPIED INSIDE ITS WRAPPER, so its type is the
|
|
600
|
+
// author's text where a rename looks for it, and a finding in it lands where written.
|
|
601
|
+
if (this.checking && current.shape === 'props') {
|
|
602
|
+
copy(current.from)
|
|
603
|
+
pin(current.from)
|
|
604
|
+
out += '__accessors('
|
|
605
|
+
origins?.push({
|
|
606
|
+
at: base + out.length,
|
|
607
|
+
source: current.from,
|
|
608
|
+
length: current.to - current.from,
|
|
609
|
+
})
|
|
610
|
+
out += source.slice(current.from, current.to)
|
|
611
|
+
pin(current.to)
|
|
612
|
+
out += this.checkedPropsClose(current)
|
|
613
|
+
at = current.to
|
|
614
|
+
continue
|
|
615
|
+
}
|
|
434
616
|
const rewritten = this.rewrite(current)
|
|
435
617
|
if (rewritten === undefined) continue
|
|
436
618
|
copy(current.from)
|
|
@@ -442,6 +624,23 @@ export class Lowering {
|
|
|
442
624
|
return out
|
|
443
625
|
}
|
|
444
626
|
|
|
627
|
+
/**
|
|
628
|
+
* 33.4 — what closes the `__accessors(` the checker's `props` call is wrapped in: the record
|
|
629
|
+
* of accessors the caller passed, with the keys the child writes (33.6) and the keys it
|
|
630
|
+
* defaults (33.20).
|
|
631
|
+
*/
|
|
632
|
+
private checkedPropsClose(read: Read): string {
|
|
633
|
+
const written =
|
|
634
|
+
read.path.length === 0
|
|
635
|
+
? ''
|
|
636
|
+
: `, ${JSON.stringify(read.path.map((key) => key.name))}`
|
|
637
|
+
const without =
|
|
638
|
+
read.keys.length === 0
|
|
639
|
+
? ''
|
|
640
|
+
: `${written === '' ? ', []' : ''}, ${JSON.stringify(read.keys)}`
|
|
641
|
+
return `${written}${without})`
|
|
642
|
+
}
|
|
643
|
+
|
|
445
644
|
/**
|
|
446
645
|
* The read's name as emitted: `name()` for a written prop, whose cell its accessor hands back.
|
|
447
646
|
*
|
|
@@ -456,6 +655,37 @@ export class Lowering {
|
|
|
456
655
|
return read.through ? `${spelled}()` : spelled
|
|
457
656
|
}
|
|
458
657
|
|
|
658
|
+
/**
|
|
659
|
+
* 32.67 — one read of a row's item: the value as written outside its row's mount, the record's
|
|
660
|
+
* value in a listener, `itemOf` the record in a binding of the row's own top level, and the
|
|
661
|
+
* cell's read everywhere else in it.
|
|
662
|
+
*/
|
|
663
|
+
private itemRead(read: Read): string | undefined {
|
|
664
|
+
const reading = this.itemModes.get(read.item)
|
|
665
|
+
if (reading === undefined) return undefined
|
|
666
|
+
const { mode, record, uses } = reading
|
|
667
|
+
const spelled = this.projection.text.slice(read.start, read.end)
|
|
668
|
+
let value = `${spelled}()`
|
|
669
|
+
if (
|
|
670
|
+
this.recording === 'listener' ||
|
|
671
|
+
(this.recording === 'binding' && mode === 'own')
|
|
672
|
+
) {
|
|
673
|
+
const from =
|
|
674
|
+
this.recording === 'listener'
|
|
675
|
+
? `${record}.data`
|
|
676
|
+
: `${this.use('itemOf')}(${record})`
|
|
677
|
+
// A PATTERN'S NAME is no member of the value at one path, so it is the list's own picker
|
|
678
|
+
// of that name (`rowPickers`), handed the record's value.
|
|
679
|
+
const text = this.projection.text
|
|
680
|
+
const named =
|
|
681
|
+
text.slice(read.item, read.item + spelled.length) === spelled &&
|
|
682
|
+
!/[\w$]/.test(text[read.item + spelled.length] ?? '')
|
|
683
|
+
value = named ? from : `${record}_${spelled}(${from})`
|
|
684
|
+
uses.record += 1
|
|
685
|
+
} else uses.cell += 1
|
|
686
|
+
return read.shape === 'shorthand' ? `${spelled}: ${value}` : value
|
|
687
|
+
}
|
|
688
|
+
|
|
459
689
|
/** What one read lowers to, or `undefined` where it stays exactly as written. */
|
|
460
690
|
private rewrite(read: Read): string | undefined {
|
|
461
691
|
const spelled = this.projection.text.slice(read.start, read.end)
|
|
@@ -498,18 +728,8 @@ export class Lowering {
|
|
|
498
728
|
// 33.20 — a defaulted key leaves the record, so its default always stands and asks the
|
|
499
729
|
// caller's accessor itself (`propOr`); its cell, where the child writes it, is built there.
|
|
500
730
|
case 'props': {
|
|
731
|
+
// The checker's is bracketed in `lower` (`checkedPropsClose`).
|
|
501
732
|
const defaulted = read.keys
|
|
502
|
-
if (this.checking) {
|
|
503
|
-
const written =
|
|
504
|
-
read.path.length === 0
|
|
505
|
-
? ''
|
|
506
|
-
: `, ${JSON.stringify(read.path.map((key) => key.name))}`
|
|
507
|
-
const without =
|
|
508
|
-
defaulted.length === 0
|
|
509
|
-
? ''
|
|
510
|
-
: `${written === '' ? ', []' : ''}, ${JSON.stringify(defaulted)}`
|
|
511
|
-
return `__accessors(${this.projection.text.slice(read.from, read.to)}${written}${without})`
|
|
512
|
-
}
|
|
513
733
|
const writtenKeys: (string | undefined)[] = []
|
|
514
734
|
for (const key of read.path)
|
|
515
735
|
if (key.name === undefined || !defaulted.includes(key.name))
|
|
@@ -63,20 +63,27 @@ export class PositionTable {
|
|
|
63
63
|
|
|
64
64
|
/** The position `node` is the whole of, or `undefined` where it is none or only part of one. */
|
|
65
65
|
of(node: Node): Position | undefined {
|
|
66
|
-
const
|
|
66
|
+
const position = this.holding(node.getStart())
|
|
67
|
+
if (position === undefined) return undefined
|
|
68
|
+
const parent = node.parent
|
|
69
|
+
return parent === undefined || parent.getStart() < position.start
|
|
70
|
+
? position
|
|
71
|
+
: undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The position whose span holds `offset`, or `undefined` where none does. */
|
|
75
|
+
holding(offset: number): Position | undefined {
|
|
67
76
|
let low = 0
|
|
68
77
|
let high = this.all.length
|
|
69
78
|
while (low < high) {
|
|
70
79
|
const middle = (low + high) >>> 1
|
|
71
|
-
if ((this.all[middle] as Position).start <=
|
|
80
|
+
if ((this.all[middle] as Position).start <= offset) low = middle + 1
|
|
72
81
|
else high = middle
|
|
73
82
|
}
|
|
74
83
|
const position = this.all[low - 1]
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
? position
|
|
79
|
-
: undefined
|
|
84
|
+
return position === undefined || offset >= position.end
|
|
85
|
+
? undefined
|
|
86
|
+
: position
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
/** Whether any position is of a kind `kinds` holds. */
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A refusal that knows WHERE in the file's text it stands, so a sink handed the text reports it on
|
|
3
|
+
* the line and column the author typed rather than at the file (`Refusals.at`).
|
|
4
|
+
*/
|
|
5
|
+
export class PositionedRefusal extends Error {
|
|
6
|
+
/** The offset into the file's text the refusal stands at. */
|
|
7
|
+
readonly offset: number
|
|
8
|
+
|
|
9
|
+
constructor(message: string, offset: number) {
|
|
10
|
+
super(message)
|
|
11
|
+
this.offset = offset
|
|
12
|
+
}
|
|
13
|
+
}
|
package/src/compiler/Refusals.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { resolve } from 'node:path' // bun exposes no path helpers of its own
|
|
2
2
|
import { formatDiagnosis } from './formatDiagnosis.ts'
|
|
3
3
|
import { diagnosisAt } from './lineOf.ts'
|
|
4
|
+
import { PositionedRefusal } from './PositionedRefusal.ts'
|
|
4
5
|
import type { Diagnosis } from './TypeSession.ts'
|
|
5
6
|
|
|
6
7
|
/**
|
|
@@ -25,18 +26,24 @@ export class Refusals {
|
|
|
25
26
|
this.collecting = collecting
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/**
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
29
|
+
/**
|
|
30
|
+
* Refuses at `file`, below the root or absolute: `thrown` is thrown, or kept. Handed the file's
|
|
31
|
+
* `text`, a `PositionedRefusal` is placed on its line and column, and thrown as that line.
|
|
32
|
+
*/
|
|
33
|
+
at(file: string, thrown: unknown, text?: string): void {
|
|
34
|
+
const positioned =
|
|
35
|
+
text !== undefined && thrown instanceof PositionedRefusal
|
|
36
|
+
if (!this.collecting && !positioned) throw thrown
|
|
37
|
+
const diagnosis = diagnosisAt(
|
|
38
|
+
resolve(this.root, file),
|
|
39
|
+
positioned ? text : undefined,
|
|
40
|
+
positioned ? thrown.offset : 0,
|
|
41
|
+
'error',
|
|
42
|
+
thrown instanceof Error ? thrown.message : String(thrown),
|
|
39
43
|
)
|
|
44
|
+
if (!this.collecting)
|
|
45
|
+
throw new Error(formatDiagnosis(diagnosis, process.cwd()))
|
|
46
|
+
this.found.push(diagnosis)
|
|
40
47
|
}
|
|
41
48
|
|
|
42
49
|
/**
|
|
@@ -24,6 +24,8 @@ export const SHARED_NAMES: ReadonlySet<string> = new Set([
|
|
|
24
24
|
// 32.5, 32.8 — what a `bind:` operand holds, which the markup writes into the tag.
|
|
25
25
|
'boundValue',
|
|
26
26
|
'groupHolds',
|
|
27
|
+
// 32.61 — a bound `<select>`'s options, marked where its value selects them.
|
|
28
|
+
'selectedMarkup',
|
|
27
29
|
// 32.6, 33.6, 33.7 — a component's `bind:`, and the cell a child writing that prop builds.
|
|
28
30
|
'boundProp',
|
|
29
31
|
'propCell',
|
|
@@ -98,6 +98,43 @@ export class ShadowMap {
|
|
|
98
98
|
? { path: this.shadowPath, position: projected }
|
|
99
99
|
: { path: this.checkedPath, position: checked }
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* EVERY place the checker reads the author's `position` at: the projection where it copied it,
|
|
104
|
+
* and the checked module too wherever the lowering copied it on — a component's props type is
|
|
105
|
+
* a second copy (38.116). A name read by name is a rewrite in the checked module, so its uses
|
|
106
|
+
* there are another symbol's — the projection's holds them.
|
|
107
|
+
*/
|
|
108
|
+
placesFrom(position: number): { path: string; position: number }[] {
|
|
109
|
+
const projected = mapForward(this.projected, position)
|
|
110
|
+
if (projected === undefined) return []
|
|
111
|
+
const places = [{ path: this.shadowPath, position: projected }]
|
|
112
|
+
for (const origin of this.checked)
|
|
113
|
+
if (
|
|
114
|
+
projected >= origin.source &&
|
|
115
|
+
projected < origin.source + origin.length
|
|
116
|
+
)
|
|
117
|
+
places.push({
|
|
118
|
+
path: this.checkedPath,
|
|
119
|
+
position: origin.at + projected - origin.source,
|
|
120
|
+
})
|
|
121
|
+
return places
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* `start` to `end` in `path`, placed in the `.abide` only where BOTH layers copied it from what
|
|
126
|
+
* the author wrote — the strict `spanToSource`, for an edit that must land on the author's text.
|
|
127
|
+
*/
|
|
128
|
+
copiedToSource(path: string, start: number, end: number): Span | undefined {
|
|
129
|
+
let projected: number | undefined = start
|
|
130
|
+
if (path === this.checkedPath)
|
|
131
|
+
projected = copiedFrom(this.checked, start)
|
|
132
|
+
if (projected === undefined) return undefined
|
|
133
|
+
const source = copiedFrom(this.projected, projected)
|
|
134
|
+
return source === undefined
|
|
135
|
+
? undefined
|
|
136
|
+
: { start: source, end: source + end - start }
|
|
137
|
+
}
|
|
101
138
|
}
|
|
102
139
|
|
|
103
140
|
/** The start of the run holding `position`, plus how far in; scaffolding is the run before's end. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every control block the template parser reads (`parseAbide`), and the arms each one takes.
|
|
3
|
+
*
|
|
4
|
+
* 38.117 — what `abide lsp` offers after `{#`, `{:` and `{/`; `templateContext.test.ts` holds the
|
|
5
|
+
* parser to the same list.
|
|
6
|
+
*/
|
|
7
|
+
export const TEMPLATE_BLOCKS: Readonly<Record<string, readonly string[]>> = {
|
|
8
|
+
if: ['else', 'else if'],
|
|
9
|
+
for: [],
|
|
10
|
+
try: ['catch', 'finally'],
|
|
11
|
+
component: [],
|
|
12
|
+
}
|