Compare commits

...
Sign in to create a new pull request.

2 commits

Author SHA1 Message Date
Robin Malfait
f7382fedbc
mutate node outside of the walk
Co-authored-by: Jordan Pittman <jordan@cryptica.me>
2025-10-15 21:34:45 +02:00
Robin Malfait
1ceea2ff77
change underlying data structure
This improves memory usage and performance. The idea is exactly the same
though. But instead of using `push()` and `pop()` we just build up 2
objects where we can track the number and parent individually based on
the `depth`.

This means that we only allocate 2 objects (which we change over time)
we then simply use `parents[depth]` and `offsets[depth]` to get the
necessary data out.

Additionally, we're only storing the index/offset and the parent now.
Since the incoming ast is `T[]`, it means that we don't really have a
parent. For this we can create a fake parent (which is never passed to
`enter(…)`, `exit(…)` or used by `path()`)
2025-10-15 21:29:35 +02:00
2 changed files with 37 additions and 22 deletions

View file

@ -447,6 +447,7 @@ export function createVariants(theme: Theme): Variants {
if (variant.modifier) return null if (variant.modifier) return null
let didApply = false let didApply = false
let replacement: AstNode | null = null
walk([ruleNode], (node, ctx) => { walk([ruleNode], (node, ctx) => {
if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue if (node.kind !== 'rule' && node.kind !== 'at-rule') return WalkAction.Continue
@ -492,7 +493,7 @@ export function createVariants(theme: Theme): Variants {
rules.push(negatedAtRule) rules.push(negatedAtRule)
} }
Object.assign(ruleNode, styleRule('&', rules)) replacement = styleRule('&', rules)
// Track that the variant was actually applied // Track that the variant was actually applied
didApply = true didApply = true
@ -500,6 +501,10 @@ export function createVariants(theme: Theme): Variants {
return WalkAction.Skip return WalkAction.Skip
}) })
if (replacement) {
Object.assign(ruleNode, replacement)
}
// TODO: Tweak group, peer, has to ignore intermediate `&` selectors (maybe?) // TODO: Tweak group, peer, has to ignore intermediate `&` selectors (maybe?)
if (ruleNode.kind === 'rule' && ruleNode.selector === '&' && ruleNode.nodes.length === 1) { if (ruleNode.kind === 'rule' && ruleNode.selector === '&' && ruleNode.nodes.length === 1) {
Object.assign(ruleNode, ruleNode.nodes[0]) Object.assign(ruleNode, ruleNode.nodes[0])

View file

@ -57,36 +57,44 @@ function walkImplementation<T extends { nodes?: T[] }>(
enter: (node: T, ctx: VisitContext<T>) => EnterResult<T> | void = () => WalkAction.Continue, enter: (node: T, ctx: VisitContext<T>) => EnterResult<T> | void = () => WalkAction.Continue,
exit: (node: T, ctx: VisitContext<T>) => ExitResult<T> | void = () => WalkAction.Continue, exit: (node: T, ctx: VisitContext<T>) => ExitResult<T> | void = () => WalkAction.Continue,
) { ) {
let stack: [nodes: T[], offset: number, parent: Parent<T> | null][] = [[ast, 0, null]] let surrogate = { nodes: ast } as Parent<T>
// Reduce memory usage by tracking 2 different objects instead of a single
// stack data structure. We could use 2 arrays, but objects are faster in Bun.
// In Node.js the 2 arrays or 2 objects have similar performance.
//
// Used indexing to prevent `push()` / `pop()` overhead.
let offsets: Record<number, number> = { 0: 0 }
let parents: Record<number, Parent<T>> = { 0: surrogate }
let depth = 0
let ctx: VisitContext<T> = { let ctx: VisitContext<T> = {
parent: null, parent: null,
depth: 0, depth: 0,
path() { path() {
let path: T[] = [] let path: T[] = []
for (let i = 1; i < stack.length; i++) { for (let i = 1; i <= depth; i++) {
let parent = stack[i][2] path.push(parents[i])
if (parent) path.push(parent)
} }
return path return path
}, },
} }
while (stack.length > 0) { while (depth >= 0) {
let depth = stack.length - 1 let offset = offsets[depth]
let frame = stack[depth] let parent = parents[depth]
let nodes = frame[0] let nodes = parent.nodes
let offset = frame[1]
let parent = frame[2]
// Done with this level // Done with this level
if (offset >= nodes.length) { if (offset >= nodes.length) {
stack.pop() depth--
continue continue
} }
ctx.parent = parent ctx.parent = depth === 0 ? null : parent
ctx.depth = depth ctx.depth = depth
// Enter phase (offsets are positive) // Enter phase (offsets are positive)
@ -96,11 +104,13 @@ function walkImplementation<T extends { nodes?: T[] }>(
switch (result.kind) { switch (result.kind) {
case WalkKind.Continue: { case WalkKind.Continue: {
if (node.nodes && node.nodes.length > 0) { offsets[depth] = ~offset // Prepare for exit phase, same offset
stack.push([node.nodes, 0, node as Parent<T>])
}
frame[1] = ~offset // Prepare for exit phase, same offset if (node.nodes && node.nodes.length > 0) {
depth++
offsets[depth] = 0
parents[depth] = node as Parent<T>
}
continue continue
} }
@ -108,7 +118,7 @@ function walkImplementation<T extends { nodes?: T[] }>(
return // Stop immediately return // Stop immediately
case WalkKind.Skip: { case WalkKind.Skip: {
frame[1] = ~offset // Prepare for exit phase, same offset offsets[depth] = ~offset // Prepare for exit phase, same offset
continue continue
} }
@ -124,7 +134,7 @@ function walkImplementation<T extends { nodes?: T[] }>(
case WalkKind.ReplaceSkip: { case WalkKind.ReplaceSkip: {
nodes.splice(offset, 1, ...result.nodes) nodes.splice(offset, 1, ...result.nodes)
frame[1] += result.nodes.length // Advance to next sibling past replacements offsets[depth] += result.nodes.length // Advance to next sibling past replacements
continue continue
} }
@ -146,7 +156,7 @@ function walkImplementation<T extends { nodes?: T[] }>(
switch (result.kind) { switch (result.kind) {
case WalkKind.Continue: case WalkKind.Continue:
frame[1] = index + 1 // Advance to next sibling offsets[depth] = index + 1 // Advance to next sibling
continue continue
case WalkKind.Stop: case WalkKind.Stop:
@ -154,7 +164,7 @@ function walkImplementation<T extends { nodes?: T[] }>(
case WalkKind.Replace: { case WalkKind.Replace: {
nodes.splice(index, 1, ...result.nodes) nodes.splice(index, 1, ...result.nodes)
frame[1] = index + result.nodes.length // Advance to next sibling past replacements offsets[depth] = index + result.nodes.length // Advance to next sibling past replacements
continue continue
} }
@ -165,7 +175,7 @@ function walkImplementation<T extends { nodes?: T[] }>(
case WalkKind.ReplaceSkip: { case WalkKind.ReplaceSkip: {
nodes.splice(index, 1, ...result.nodes) nodes.splice(index, 1, ...result.nodes)
frame[1] = index + result.nodes.length // Advance to next sibling past replacements offsets[depth] = index + result.nodes.length // Advance to next sibling past replacements
continue continue
} }