Skip to content

Commit 36fc866

Browse files
authored
Check return types (#615)
1 parent adc69b2 commit 36fc866

File tree

9 files changed

+628
-136
lines changed

9 files changed

+628
-136
lines changed

spec/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ <h1>Structured Headers</h1>
212212
<li><b>for:</b> The type of value to which a clause of type "concrete method" or "internal method" applies.</li>
213213
<li><b>redefinition:</b> If "true", the name of the operation will not automatically link (i.e., it will not automatically be given an aoid).</li>
214214
<li><b>skip global checks:</b> If "true", disables consistency checks for this AO which require knowing every callsite.</li>
215+
<li><b>skip return checks:</b> If "true", disables checking that the returned values from this AO correspond to its declared return type. Adding this to an AO which does not require it will produce a warning.</li>
215216
</ul>
216217
</li>
217218
</ol>

src/Algorithm.ts

Lines changed: 0 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -68,72 +68,6 @@ export default class Algorithm extends Builder {
6868
namespace,
6969
}));
7070
spec._ntStringRefs = spec._ntStringRefs.concat(nonterminals);
71-
72-
const returnType = clause?.signature?.return;
73-
let containsAnyCompletionyThings = false;
74-
if (returnType?.kind != null) {
75-
function checkForCompletionyStuff(list: emd.OrderedListNode) {
76-
for (const step of list.contents) {
77-
if (
78-
step.contents[0].name === 'text' &&
79-
/^(note|assert):/i.test(step.contents[0].contents)
80-
) {
81-
continue;
82-
}
83-
if (
84-
step.contents.some(
85-
c => c.name === 'text' && /a new (\w+ )?Abstract Closure/i.test(c.contents),
86-
)
87-
) {
88-
continue;
89-
}
90-
for (const part of step.contents) {
91-
if (part.name !== 'text') {
92-
continue;
93-
}
94-
const completionyThing = part.contents.match(
95-
/\b(ReturnIfAbrupt\b|(^|(?<=, ))[tT]hrow (a\b|the\b|$)|[rR]eturn (Normal|Throw|Return)?Completion\(|[rR]eturn( a| a new| the)? Completion Record\b|the result of evaluating\b)|(?<=[\s(])\?\s/,
96-
);
97-
if (completionyThing != null) {
98-
if (returnType?.kind === 'completion') {
99-
containsAnyCompletionyThings = true;
100-
} else {
101-
spec.warn({
102-
type: 'contents',
103-
ruleId: 'completiony-thing-in-non-completion-algorithm',
104-
message:
105-
'this would return a Completion Record, but the containing AO is declared not to return a Completion Record',
106-
node,
107-
nodeRelativeLine: part.location.start.line,
108-
nodeRelativeColumn: part.location.start.column + completionyThing.index!,
109-
});
110-
}
111-
}
112-
}
113-
if (step.sublist?.name === 'ol') {
114-
checkForCompletionyStuff(step.sublist);
115-
}
116-
}
117-
}
118-
checkForCompletionyStuff(emdTree.contents);
119-
120-
// TODO: remove 'GeneratorYield' when the spec is more coherent (https://github.com/tc39/ecma262/pull/2429)
121-
// TODO: remove SDOs after doing the work necessary to coordinate the `containsAnyCompletionyThings` bit across all the piecewise components of an SDO's definition
122-
if (
123-
!['Completion', 'GeneratorYield'].includes(clause.aoid!) &&
124-
returnType?.kind === 'completion' &&
125-
!containsAnyCompletionyThings &&
126-
!['sdo', 'internal method', 'concrete method'].includes(clause.type!)
127-
) {
128-
spec.warn({
129-
type: 'node',
130-
ruleId: 'completion-algorithm-lacks-completiony-thing',
131-
message:
132-
'this algorithm is declared as returning a Completion Record, but there is no step which might plausibly return an abrupt completion',
133-
node,
134-
});
135-
}
136-
}
13771
}
13872

13973
const rawHtml = emd.emit(emdTree);

src/Biblio.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,8 +271,10 @@ export default class Biblio {
271271
delete copy.location;
272272
// @ts-ignore
273273
delete copy.referencingIds;
274-
// @ts-ignore
275-
delete copy._node;
274+
for (const key of Object.keys(copy)) {
275+
// @ts-ignore
276+
if (key.startsWith('_')) delete copy[key];
277+
}
276278
return copy;
277279
}),
278280
};
@@ -344,6 +346,7 @@ export interface AlgorithmBiblioEntry extends BiblioEntryBase {
344346
signature: null | Signature;
345347
effects: string[];
346348
skipGlobalChecks?: boolean;
349+
/** @internal*/ _skipReturnChecks?: boolean;
347350
/** @internal*/ _node?: Element;
348351
}
349352

@@ -398,7 +401,7 @@ export type PartialBiblioEntry = Unkey<BiblioEntry, NonExportedKeys>;
398401

399402
export type ExportedBiblio = {
400403
location: string;
401-
entries: PartialBiblioEntry[];
404+
entries: Unkey<PartialBiblioEntry, `_${string}`>[];
402405
};
403406

404407
function dumpEnv(env: EnvRec) {

src/Clause.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ export default class Clause extends Builder {
5555
/** @internal */ readonly effects: string[]; // this is held by identity and mutated by Spec.ts
5656
/** @internal */ signature: Signature | null;
5757
/** @internal */ skipGlobalChecks: boolean;
58+
/** @internal */ skipReturnChecks: boolean;
5859

5960
constructor(spec: Spec, node: HTMLElement, parent: Clause, number: string) {
6061
super(spec, node);
@@ -67,6 +68,7 @@ export default class Clause extends Builder {
6768
this.examples = [];
6869
this.effects = [];
6970
this.skipGlobalChecks = false;
71+
this.skipReturnChecks = false;
7072

7173
// namespace is either the entire spec or the parent clause's namespace.
7274
let parentNamespace = spec.namespace;
@@ -206,6 +208,7 @@ export default class Clause extends Builder {
206208
effects,
207209
redefinition,
208210
skipGlobalChecks,
211+
skipReturnChecks,
209212
} = parseStructuredHeaderDl(this.spec, type, dl);
210213

211214
const paras = formatPreamble(
@@ -237,6 +240,7 @@ export default class Clause extends Builder {
237240
}
238241

239242
this.skipGlobalChecks = skipGlobalChecks;
243+
this.skipReturnChecks = skipReturnChecks;
240244

241245
this.effects.push(...effects);
242246
for (const effect of effects) {
@@ -373,6 +377,7 @@ export default class Clause extends Builder {
373377
signature,
374378
effects: clause.effects,
375379
_node: clause.node,
380+
_skipReturnChecks: clause.skipReturnChecks,
376381
};
377382
if (clause.skipGlobalChecks) {
378383
op.skipGlobalChecks = true;

src/expr-parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const periodSpaceMatcher = /(?<period>\.(?= ))/u;
77
const periodSpaceOrEOFMatcher = /(?<period>\.(?= |$))/u;
88

99
type SimpleLocation = { start: { offset: number }; end: { offset: number } };
10-
type BareText = {
10+
export type BareText = {
1111
name: 'text';
1212
contents: string;
1313
location: SimpleLocation;

src/header-parser.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,14 @@ export function parseStructuredHeaderDl(
407407
effects: string[];
408408
redefinition: boolean;
409409
skipGlobalChecks: boolean;
410+
skipReturnChecks: boolean;
410411
} {
411412
let description = null;
412413
let _for = null;
413414
let redefinition: boolean | null = null;
414415
let effects: string[] = [];
415416
let skipGlobalChecks: boolean | null = null;
417+
let skipReturnChecks: boolean | null = null;
416418
for (let i = 0; i < dl.children.length; ++i) {
417419
const dt = dl.children[i];
418420
if (dt.tagName !== 'DT') {
@@ -482,6 +484,7 @@ export function parseStructuredHeaderDl(
482484
}
483485
break;
484486
}
487+
// TODO figure out how to de-dupe the code for boolean attributes
485488
case 'redefinition': {
486489
if (redefinition != null) {
487490
spec.warn({
@@ -538,6 +541,34 @@ export function parseStructuredHeaderDl(
538541
}
539542
break;
540543
}
544+
case 'skip return checks': {
545+
if (skipReturnChecks != null) {
546+
spec.warn({
547+
type: 'node',
548+
ruleId: 'header-format',
549+
message: `duplicate "skip return checks" attribute`,
550+
node: dt,
551+
});
552+
}
553+
const contents = (dd.textContent ?? '').trim();
554+
if (contents === 'true') {
555+
skipReturnChecks = true;
556+
} else if (contents === 'false') {
557+
skipReturnChecks = false;
558+
} else {
559+
spec.warn({
560+
type: 'contents',
561+
ruleId: 'header-format',
562+
message: `unknown value for "skip return checks" attribute (expected "true" or "false", got ${JSON.stringify(
563+
contents,
564+
)})`,
565+
node: dd,
566+
nodeRelativeLine: 1,
567+
nodeRelativeColumn: 1,
568+
});
569+
}
570+
break;
571+
}
541572
case '': {
542573
spec.warn({
543574
type: 'node',
@@ -564,6 +595,7 @@ export function parseStructuredHeaderDl(
564595
effects,
565596
redefinition: redefinition ?? false,
566597
skipGlobalChecks: skipGlobalChecks ?? false,
598+
skipReturnChecks: skipReturnChecks ?? false,
567599
};
568600
}
569601

src/type-logic.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type Biblio from './Biblio';
22
import type { Type as BiblioType } from './Biblio';
33
import type { Expr, NonSeq } from './expr-parser';
44

5-
type Type =
5+
export type Type =
66
| { kind: 'unknown' } // top
77
| { kind: 'never' } // bottom
88
| { kind: 'union'; of: NonUnion[] } // constraint: nothing in the union dominates anything else in the union
@@ -80,6 +80,7 @@ const dominateGraph: Partial<Record<Type['kind'], Type['kind'][]>> = {
8080
bigint: ['concrete bigint'],
8181
boolean: ['concrete boolean'],
8282
};
83+
8384
/*
8485
The type lattice used here is very simple (aside from explicit unions).
8586
As such we mostly only need to define the `dominates` relationship and apply trivial rules:
@@ -140,6 +141,7 @@ export function dominates(a: Type, b: Type): boolean {
140141
}
141142
return false;
142143
}
144+
143145
function addToUnion(types: NonUnion[], type: NonUnion): Type {
144146
if (type.kind === 'normal completion') {
145147
const existingNormalCompletionIndex = types.findIndex(t => t.kind === 'normal completion');
@@ -583,8 +585,7 @@ export function typeFromExprType(type: BiblioType): Type {
583585
break;
584586
}
585587
case 'unused': {
586-
// this is really only a return type, but might as well handle it
587-
return { kind: 'enum value', value: '~unused~' };
588+
return { kind: 'enum value', value: 'unused' };
588589
}
589590
}
590591
return { kind: 'unknown' };
@@ -600,15 +601,24 @@ export function isCompletion(
600601
);
601602
}
602603

604+
export function isPossiblyAbruptCompletion(
605+
type: Type,
606+
): type is Type & { kind: 'abrupt completion' | 'union' } {
607+
return (
608+
type.kind === 'abrupt completion' ||
609+
(type.kind === 'union' && type.of.some(isPossiblyAbruptCompletion))
610+
);
611+
}
612+
603613
export function stripWhitespace(items: NonSeq[]) {
604614
items = [...items];
605-
while (items[0]?.name === 'text' && /^\s+$/.test(items[0].contents)) {
615+
while (items[0]?.name === 'text' && /^\s*$/.test(items[0].contents)) {
606616
items.shift();
607617
}
608618
while (
609619
items[items.length - 1]?.name === 'text' &&
610620
// @ts-expect-error
611-
/^\s+$/.test(items[items.length - 1].contents)
621+
/^\s*$/.test(items[items.length - 1].contents)
612622
) {
613623
items.pop();
614624
}

0 commit comments

Comments
 (0)