From 1888bd95ad043928a20c0a68800fa01d7e53c8c2 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Sat, 28 Mar 2026 11:28:18 -0600 Subject: [PATCH 1/3] add a visual indicator to AC bodies --- css/elements.css | 10 ++++++++++ src/Algorithm.ts | 19 +++++++++++++++++++ .../generated-reference/assets-inline.html | 10 ++++++++++ .../generated-reference/effect-user-code.html | 2 +- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/css/elements.css b/css/elements.css index cda2b290..4853fbd9 100644 --- a/css/elements.css +++ b/css/elements.css @@ -75,6 +75,8 @@ --attributes-tag-foreground-color: #884400; + --ac-body-border-color: #ddd; + --figure-background: #fff; } @@ -157,6 +159,8 @@ --attributes-tag-foreground-color: #e6a96d; + --ac-body-border-color: #555; + --figure-background: #fff; } } @@ -496,6 +500,12 @@ emu-alg ol.nested-lots ol { list-style-type: lower-roman; } +emu-alg ol.ac-body { + border-left: 6px double var(--ac-body-border-color); + border-radius: 0 0 0 10px; + margin-bottom: 5px; +} + emu-eqn { display: block; margin-left: 4em; diff --git a/src/Algorithm.ts b/src/Algorithm.ts index e6560d86..8ddb0050 100644 --- a/src/Algorithm.ts +++ b/src/Algorithm.ts @@ -119,6 +119,16 @@ export default class Algorithm extends Builder { } } + for (const step of node.querySelectorAll('li')) { + if (/\ba new Abstract Closure\b/.test(ownTextContent(step))) { + for (const ol of step.children) { + if (ol.tagName === 'OL') { + ol.classList.add('ac-body'); + } + } + } + } + for (const step of node.querySelectorAll(kindSelector)) { // prettier-ignore const attributes = SPECIAL_KINDS @@ -153,6 +163,15 @@ export default class Algorithm extends Builder { static readonly elements = ['EMU-ALG'] as const; } +function ownTextContent(el: Element): string { + let text = ''; + for (const child of el.childNodes) { + if (child.nodeType === 1 && (child as Element).tagName === 'OL') continue; + text += child.textContent; + } + return text; +} + function getStepNumbers(item: Element) { const { indexOf } = Array.prototype; const counts = []; diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index cbc629c9..7236d348 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -1693,6 +1693,8 @@ --attributes-tag-foreground-color: #884400; + --ac-body-border-color: #ddd; + --figure-background: #fff; } @@ -1775,6 +1777,8 @@ --attributes-tag-foreground-color: #e6a96d; + --ac-body-border-color: #555; + --figure-background: #fff; } } @@ -2114,6 +2118,12 @@ list-style-type: lower-roman; } +emu-alg ol.ac-body { + border-left: 6px double var(--ac-body-border-color); + border-radius: 0 0 0 10px; + margin-bottom: 5px; +} + emu-eqn { display: block; margin-left: 4em; diff --git a/test/baselines/generated-reference/effect-user-code.html b/test/baselines/generated-reference/effect-user-code.html index fa5442ab..6fe949a0 100644 --- a/test/baselines/generated-reference/effect-user-code.html +++ b/test/baselines/generated-reference/effect-user-code.html @@ -111,7 +111,7 @@

16 RenderedMeta ( )

17 MakeAbstractClosure ( )

The abstract operation MakeAbstractClosure takes no arguments. The user-code effect doesn't propagate through Abstract Closure boundaries by recognizing the "be a new Abstract Closure" substring. It performs the following steps when called:

-
  1. Let closure be a new Abstract Closure that captures nothing and performs the following steps when called:
    1. UserCode().
  2. Return closure.
+
  1. Let closure be a new Abstract Closure that captures nothing and performs the following steps when called:
    1. UserCode().
  2. Return closure.
From 4e1fd6630b5e5a618e074bc43def21be3ac37120 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Sat, 28 Mar 2026 19:30:46 -0600 Subject: [PATCH 2/3] revert to original styling --- css/elements.css | 6 +++--- test/baselines/generated-reference/assets-inline.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/css/elements.css b/css/elements.css index 4853fbd9..6384489b 100644 --- a/css/elements.css +++ b/css/elements.css @@ -501,9 +501,9 @@ emu-alg ol.nested-lots ol { } emu-alg ol.ac-body { - border-left: 6px double var(--ac-body-border-color); - border-radius: 0 0 0 10px; - margin-bottom: 5px; + border-left: 3px solid var(--ac-body-border-color); + border-radius: 0 0 0 20px; + padding-bottom: 5px; } emu-eqn { diff --git a/test/baselines/generated-reference/assets-inline.html b/test/baselines/generated-reference/assets-inline.html index 7236d348..42e33c39 100644 --- a/test/baselines/generated-reference/assets-inline.html +++ b/test/baselines/generated-reference/assets-inline.html @@ -2119,9 +2119,9 @@ } emu-alg ol.ac-body { - border-left: 6px double var(--ac-body-border-color); - border-radius: 0 0 0 10px; - margin-bottom: 5px; + border-left: 3px solid var(--ac-body-border-color); + border-radius: 0 0 0 20px; + padding-bottom: 5px; } emu-eqn { From a38c06df6149339e93fc616cf86bbc7ce50c9e76 Mon Sep 17 00:00:00 2001 From: Michael Ficarra Date: Sat, 28 Mar 2026 19:52:49 -0600 Subject: [PATCH 3/3] extract some helpers --- src/Algorithm.ts | 13 ++----------- src/lint/rules/variable-use-def.ts | 7 ++----- src/utils.ts | 19 +++++++++++++++---- .../generated-reference/effect-user-code.html | 2 +- 4 files changed, 20 insertions(+), 21 deletions(-) diff --git a/src/Algorithm.ts b/src/Algorithm.ts index 8ddb0050..9a5df160 100644 --- a/src/Algorithm.ts +++ b/src/Algorithm.ts @@ -4,7 +4,7 @@ import type { PartialBiblioEntry, StepBiblioEntry } from './Biblio'; import Builder from './Builder'; import { SPECIAL_KINDS_MAP, SPECIAL_KINDS } from './Clause'; -import { warnEmdFailure, wrapEmdFailure } from './utils'; +import { isAbstractClosureHeader, ownTextContent, warnEmdFailure, wrapEmdFailure } from './utils'; import { collectNonterminalsFromEmd } from './lint/utils'; import * as emd from 'ecmarkdown'; @@ -120,7 +120,7 @@ export default class Algorithm extends Builder { } for (const step of node.querySelectorAll('li')) { - if (/\ba new Abstract Closure\b/.test(ownTextContent(step))) { + if (isAbstractClosureHeader(ownTextContent(step))) { for (const ol of step.children) { if (ol.tagName === 'OL') { ol.classList.add('ac-body'); @@ -163,15 +163,6 @@ export default class Algorithm extends Builder { static readonly elements = ['EMU-ALG'] as const; } -function ownTextContent(el: Element): string { - let text = ''; - for (const child of el.childNodes) { - if (child.nodeType === 1 && (child as Element).tagName === 'OL') continue; - text += child.textContent; - } - return text; -} - function getStepNumbers(item: Element) { const { indexOf } = Array.prototype; const counts = []; diff --git a/src/lint/rules/variable-use-def.ts b/src/lint/rules/variable-use-def.ts index a5fc0a9f..abb7b21b 100644 --- a/src/lint/rules/variable-use-def.ts +++ b/src/lint/rules/variable-use-def.ts @@ -8,7 +8,7 @@ import type { import type { Reporter } from '../algorithm-error-reporter-type'; import type { Seq } from '../../expr-parser'; import { walk as walkExpr } from '../../expr-parser'; -import { offsetToLineAndColumn } from '../../utils'; +import { isAbstractClosureHeader, offsetToLineAndColumn } from '../../utils'; /* Ecmaspeak scope rules are a bit weird. @@ -244,10 +244,7 @@ function walkAlgorithm( } // handle abstract closures - if ( - last?.name === 'text' && - / performs the following steps (atomically )?when called:$/.test(last.contents) - ) { + if (last?.name === 'text' && isAbstractClosureHeader(last.contents)) { if (first.name === 'text' && first.contents === 'Let ' && isVariable(expr.items[1])) { const closureName = expr.items[1]; scope.declare(closureName.contents, closureName); diff --git a/src/utils.ts b/src/utils.ts index bd0a4bb5..008c3d27 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -284,10 +284,7 @@ export function doesEffectPropagateToParent(node: Element, effect: string) { // This is super hacky. It's checking the output of ecmarkdown. if (parent.tagName !== 'LI') continue; - if ( - effect === 'user-code' && - /be a new (\w+ )*Abstract Closure/.test(parent.textContent ?? '') - ) { + if (effect === 'user-code' && isAbstractClosureHeader(ownTextContent(parent))) { return false; } @@ -343,3 +340,17 @@ export function withOrdinalSuffix(n: number): string { const suffixes = { one: 'st', two: 'nd', few: 'rd', other: 'th' }; return `${n}${suffixes[rule as keyof typeof suffixes]}`; } + +const acHeaderRe = / performs the following steps (atomically )?when called:$/; +export function isAbstractClosureHeader(text: string): boolean { + return acHeaderRe.test(text); +} + +export function ownTextContent(el: Element): string { + let text = ''; + for (const child of el.childNodes) { + if (child.nodeType === 1 && (child as Element).tagName === 'OL') continue; + text += child.textContent; + } + return text; +} diff --git a/test/baselines/generated-reference/effect-user-code.html b/test/baselines/generated-reference/effect-user-code.html index 6fe949a0..10c992d1 100644 --- a/test/baselines/generated-reference/effect-user-code.html +++ b/test/baselines/generated-reference/effect-user-code.html @@ -117,7 +117,7 @@

17 MakeAbstractClosure ( )

18 MakeJobAbstractClosure ( )

The abstract operation MakeJobAbstractClosure takes no arguments. The user-code effect doesn't propagate through specialized Abstract Closure boundaries by recognizing the "be a new Something Abstract Closure" substring. It performs the following steps when called:

-
  1. Let closure be a new Job Abstract Closure that captures nothing and performs the following steps when called:
    1. UserCode().
  2. Return closure.
+
  1. Let closure be a new Job Abstract Closure that captures nothing and performs the following steps when called:
    1. UserCode().
  2. Return closure.