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
16 changes: 16 additions & 0 deletions css/elements.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,22 @@ a:hover {
color: #239dee;
}

a.e-user-code {
position: relative;
}

a.e-user-code::before {
display: none;
color: brown;
position: absolute;
bottom: -0.9em;
left: 0;
content: 'UC';
font-style: italic;
font-weight: bold;
font-size: small;
}

code {
font-weight: bold;
font-family: Consolas, Monaco, monospace;
Expand Down
16 changes: 15 additions & 1 deletion js/menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -964,6 +964,8 @@ function makeLinkToId(id) {
return (targetSec === 'index' ? './' : targetSec + '.html') + hash;
}

let stylesheetWorkaroundForCanCallUserCodeAnnotation;

function doShortcut(e) {
if (!(e.target instanceof HTMLElement)) {
return;
Expand All @@ -973,7 +975,10 @@ function doShortcut(e) {
if (name === 'textarea' || name === 'input' || name === 'select' || target.isContentEditable) {
return;
}
if (e.key === 'm' && !e.altKey && !e.ctrlKey && !e.metaKey && !e.shiftKey && usesMultipage) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) {
return;
}
if (e.key === 'm' && usesMultipage) {
let pathParts = location.pathname.split('/');
let hash = location.hash;
if (pathParts[pathParts.length - 2] === 'multipage') {
Expand All @@ -990,6 +995,13 @@ function doShortcut(e) {
} else {
location = 'multipage/' + hash;
}
} else if (e.key === 'u') {
if (stylesheetWorkaroundForCanCallUserCodeAnnotation.innerText === '') {
stylesheetWorkaroundForCanCallUserCodeAnnotation.textContent =
'a.e-uc::before { display: block !important; }';
} else {
stylesheetWorkaroundForCanCallUserCodeAnnotation.textContent = '';
}
}
}

Expand Down Expand Up @@ -1017,4 +1029,6 @@ document.addEventListener('keypress', doShortcut);
document.addEventListener('DOMContentLoaded', () => {
Toolbox.init();
referencePane.init();
stylesheetWorkaroundForCanCallUserCodeAnnotation = document.createElement('style');
document.head.appendChild(stylesheetWorkaroundForCanCallUserCodeAnnotation);
});
30 changes: 28 additions & 2 deletions src/Clause.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ export default class Clause extends Builder {
subclauses: Clause[];
number: string;
aoid: string | null;
type: string | null;
notes: Note[];
editorNotes: Note[];
examples: Example[];
effects: string[];

constructor(spec: Spec, node: HTMLElement, parent: Clause, number: string) {
super(spec, node);
Expand All @@ -33,6 +35,7 @@ export default class Clause extends Builder {
this.notes = [];
this.editorNotes = [];
this.examples = [];
this.effects = [];

// namespace is either the entire spec or the parent clause's namespace.
let parentNamespace = spec.namespace;
Expand All @@ -53,6 +56,11 @@ export default class Clause extends Builder {
this.aoid = node.id;
}

this.type = node.getAttribute('type');
if (this.type === '') {
this.type = null;
}

let header = this.node.firstElementChild;
while (header != null && header.tagName === 'SPAN' && header.children.length === 0) {
// skip oldids
Expand Down Expand Up @@ -87,7 +95,7 @@ export default class Clause extends Builder {
}
// if we find such a DL, treat this as a structured header

const type = this.node.getAttribute('type');
const type = this.type;

const { name, formattedHeader, formattedParams, formattedReturnType } = parseStructuredHeaderH1(
this.spec,
Expand Down Expand Up @@ -120,7 +128,7 @@ export default class Clause extends Builder {
header.innerHTML = formattedHeader;
}

const { description, for: _for } = parseStructuredHeaderDl(this.spec, type, dl);
const { description, for: _for, effects } = parseStructuredHeaderDl(this.spec, type, dl);

const paras = formatPreamble(
this.spec,
Expand Down Expand Up @@ -158,6 +166,14 @@ export default class Clause extends Builder {
this.node.setAttribute('aoid', name);
this.aoid = name;
}

this.effects = effects;
for (const effect of effects) {
if (!this.spec._effectWorklist.has(effect)) {
this.spec._effectWorklist.set(effect, []);
}
this.spec._effectWorklist.get(effect)!.push(this);
}
}

buildNotes() {
Expand All @@ -184,6 +200,16 @@ export default class Clause extends Builder {
}
}

canHaveEffect(effectName: string) {
// The following effects are runtime only:
//
// user-code: Only runtime can call user code.
if (this.title !== null && this.title.startsWith('Static Semantics:')) {
if (effectName === 'user-code') return false;
}
return true;
}

static async enter({ spec, node, clauseStack, clauseNumberer }: Context) {
if (!node.id) {
spec.warn({
Expand Down
61 changes: 61 additions & 0 deletions src/Spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ function isEmuImportElement(node: Node): node is EmuImportElement {
return node.nodeType === 1 && node.nodeName === 'EMU-IMPORT';
}

function maybeAddClauseToEffectWorklist(effectName: string, clause: Clause, worklist: Clause[]) {
if (
!worklist.includes(clause) &&
clause.canHaveEffect(effectName) &&
!clause.effects.includes(effectName)
) {
clause.effects.push(effectName);
worklist.push(clause);
}
}

/*@internal*/
export default class Spec {
spec: this;
Expand Down Expand Up @@ -290,6 +301,8 @@ export default class Spec {
}[];
_prodRefs: ProdRef[];
_textNodes: { [s: string]: [TextNodeContext] };
_effectWorklist: Map<string, Clause[]>;
_effectfulAOs: Map<string, Clause>;
refsByClause: { [refId: string]: [string] };

private _fetch: (file: string, token: CancellationToken) => PromiseLike<string>;
Expand Down Expand Up @@ -332,6 +345,8 @@ export default class Spec {
this._ntStringRefs = [];
this._prodRefs = [];
this._textNodes = {};
this._effectWorklist = new Map();
this._effectfulAOs = new Map();
this.refsByClause = Object.create(null);

this.processMetadata();
Expand Down Expand Up @@ -455,6 +470,8 @@ export default class Spec {

this.autolink();

this.log('Propagating can-call-user-code annotations...');
this.propagateEffects();
this.log('Linking xrefs...');
this._xrefs.forEach(xref => xref.build());
this.log('Linking non-terminal references...');
Expand Down Expand Up @@ -652,6 +669,50 @@ export default class Spec {
return true;
}

private propagateEffects() {
for (const [effectName, worklist] of this._effectWorklist) {
this.propagateEffect(effectName, worklist);
}
}

private propagateEffect(effectName: string, worklist: Clause[]) {
const usersOfAoid: Map<string, Set<Clause>> = new Map();
for (const xref of this._xrefs) {
if (xref.clause == null || xref.aoid == null) continue;
if (!xref.canHaveEffect(effectName)) continue;

if (xref.hasAddedEffect(effectName)) {
maybeAddClauseToEffectWorklist(effectName, xref.clause, worklist);
}

const usedAoid = xref.aoid;
if (!usersOfAoid.has(usedAoid)) {
usersOfAoid.set(usedAoid, new Set());
}
usersOfAoid.get(usedAoid)!.add(xref.clause);
}

while (worklist.length !== 0) {
const clause = worklist.shift() as Clause;
const aoid = clause.aoid;
if (aoid == null || !usersOfAoid.has(aoid)) {
continue;
}

this._effectfulAOs.set(aoid, clause);
for (const userClause of usersOfAoid.get(aoid)!) {
maybeAddClauseToEffectWorklist(effectName, userClause, worklist);
}
}
}

public getEffectsByAoid(aoid: string): string[] | null {
if (this._effectfulAOs.has(aoid)) {
return this._effectfulAOs.get(aoid)!.effects;
}
return null;
}

private async buildMultipage(wrapper: Element, tocEles: Element[], jsSha: string) {
let stillIntro = true;
const introEles = [];
Expand Down
103 changes: 97 additions & 6 deletions src/Xref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@ import type * as Biblio from './Biblio';
import type Clause from './Clause';

import Builder from './Builder';
import { validateEffects } from './utils';

/*@internal*/
export default class Xref extends Builder {
namespace: string;
href: string;
aoid: string;
isInvocation: boolean;
clause: Clause | null;
id: string;
entry: Biblio.BiblioEntry | undefined;
addEffects: string[] | null;
suppressEffects: string[] | null;

static elements = ['EMU-XREF'];

Expand All @@ -30,6 +34,61 @@ export default class Xref extends Builder {
this.aoid = aoid;
this.clause = clause;
this.id = node.getAttribute('id')!;
this.isInvocation = node.hasAttribute('is-invocation');
node.removeAttribute('is-invocation');

// Check if there's metadata adding or suppressing effects
this.addEffects = null;
this.suppressEffects = null;
if (node.parentElement && node.parentElement.tagName === 'EMU-META') {
if (node.parentElement.hasAttribute('effects')) {
const addEffects = node.parentElement.getAttribute('effects')!.split(',');
if (addEffects.length !== 0) {
this.addEffects = validateEffects(spec, addEffects, node.parentElement);
}
}
if (node.parentElement.hasAttribute('suppress-effects')) {
const suppressEffects = node.parentElement.getAttribute('suppress-effects')!.split(',');
if (suppressEffects.length !== 0) {
this.suppressEffects = validateEffects(spec, suppressEffects, node.parentElement);
}
}
if (this.addEffects !== null && this.suppressEffects !== null) {
for (const e of this.addEffects) {
if (this.suppressEffects.includes(e)) {
throw new Error('effect suppression is contradictory');
}
}
for (const e of this.suppressEffects) {
if (this.addEffects.includes(e)) {
throw new Error('effect suppression is contradictory');
}
}
}

// Strip an outer <emu-meta> if present
const children = node.parentElement.childNodes;
node.parentElement.replaceWith(...children);
}
}

canHaveEffect(effectName: string) {
if (!this.isInvocation) return false;
if (this.clause && !this.clause.canHaveEffect(effectName)) {
return false;
}
if (this.suppressEffects !== null) {
return !this.suppressEffects.includes(effectName);
}
return true;
}

hasAddedEffect(effectName: string) {
if (!this.isInvocation) return false;
if (this.addEffects !== null) {
return this.addEffects.includes(effectName);
}
return false;
}

static async enter({ node, spec, clauseStack }: Context) {
Expand Down Expand Up @@ -141,7 +200,34 @@ export default class Xref extends Builder {
this.entry = spec.biblio.byAoid(aoid, namespace);

if (this.entry) {
buildAOLink(node, this.entry);
let effects = null;
let classNames = null;

if (this.isInvocation) {
effects = spec.getEffectsByAoid(aoid);
}
if (this.addEffects !== null) {
if (effects !== null) {
effects.push(...this.addEffects);
} else {
effects = this.addEffects;
}
}

if (effects) {
if (this.suppressEffects !== null) {
effects = effects.filter(e => !this.suppressEffects!.includes(e));
}
if (effects.length !== 0) {
const parentClause = this.clause;
effects = parentClause ? effects.filter(e => parentClause.canHaveEffect(e)) : effects;
if (effects.length !== 0) {
classNames = effects.map(e => `e-${e}`).join(' ');
}
}
}

buildAOLink(node, this.entry, classNames);
return;
}

Expand Down Expand Up @@ -179,11 +265,11 @@ function buildProductionLink(xref: Element, entry: Biblio.ProductionBiblioEntry)
}
}

function buildAOLink(xref: Element, entry: Biblio.BiblioEntry) {
function buildAOLink(xref: Element, entry: Biblio.BiblioEntry, classNames: string | null) {
if (xref.textContent!.trim() === '') {
xref.innerHTML = buildXrefLink(entry, xref.getAttribute('aoid'));
xref.innerHTML = buildXrefLink(entry, xref.getAttribute('aoid'), classNames);
} else {
xref.innerHTML = buildXrefLink(entry, xref.innerHTML);
xref.innerHTML = buildXrefLink(entry, xref.innerHTML, classNames);
}
}

Expand Down Expand Up @@ -276,6 +362,11 @@ function buildStepLink(spec: Spec, xref: Element, entry: Biblio.StepBiblioEntry)
xref.innerHTML = buildXrefLink(entry, text);
}

function buildXrefLink(entry: Biblio.BiblioEntry, contents: string | number | undefined | null) {
return '<a href="' + entry.location + '#' + (entry.id || entry.refId) + '">' + contents + '</a>';
function buildXrefLink(
entry: Biblio.BiblioEntry,
contents: string | number | undefined | null,
classNames: string | null = null
) {
const classSnippet = classNames == null ? '' : ' class="' + classNames + '"';
return `<a href="${entry.location}#${entry.id || entry.refId}"${classSnippet}>${contents}</a>`;
}
Loading