// Type recognizer: scans a TypeScript type (or type parameter list, type // argument list, heritage list, object type body) and reports where it ends. // It never emits; the parser blanks the range afterwards. Every function // takes the current lexer token as its first token and leaves the lexer on // the first token after the construct, with `typeEnd` at the end of the last // token consumed. A false return means either a syntax mismatch or that the // lexer ran out of input (`more` is then set); callers check `more`. import { scan, more, tt, ts, te, tk, tb, tnl, rescanTemplate, rescanLess, reseek, tokenIn, T_EOF, T_NAME, T_NUM, T_STR, T_TEMPLATE, T_TEMPLATE_HEAD, T_TEMPLATE_MID, T_TEMPLATE_TAIL, T_LBRACE, T_RBRACE, T_LPAREN, T_RPAREN, T_LBRACK, T_RBRACK, T_DOT, T_QDOT, T_ELLIPSIS, T_SEMI, T_COMMA, T_COLON, T_QUESTION, T_ARROW, T_BANG, T_LT, T_SHL, T_GT, T_EQ, T_PLUS, T_MINUS, T_STAR, T_BAR, T_AMP, T_PRIVATE, } from './lex.zena'; import { K_EXTENDS, K_KEYOF, K_UNIQUE, K_READONLY, K_INFER, K_TYPEOF, K_NEW, K_ABSTRACT, K_IMPORT, K_THIS, K_FUNCTION, K_IS, K_ASSERTS, K_IN, K_OUT, K_AS, K_VOID, K_NULL, K_TRUE, K_FALSE, K_FIRST_CONTEXTUAL, K_IMPLEMENTS, KS_STMT_MOD, keywordIn, keywordInHi, KS_KEYWORD_TYPE, KS_KEYWORD_TYPE_HI, KS_TYPE_START, KS_TYPE_MEMBER_MOD, TS_TYPE_START, TS_PROPERTY_NAME, TS_OPEN, TS_CLOSE, TS_BINDING_START, TS_PARAM_START, TS_STR_NUM_TPL, TS_JSDOC_NULLABLE_END, TS_PARAM_FOLLOW, TS_MEMBER_NAME, KS_NO_DOT_TYPE, } from './keywords.zena'; export var typeEnd: i32 = 0; var noCond: i32 = 0; // The branch keeps this out of line: zena inlines a straight-line body this small at every call site, and // there are about 100 (516 bytes in build/erase.wasm). function nx(): boolean { typeEnd = te; scan(); if (more != 0) return false; return true; } function is(t: i32): boolean { return tt == t; } function kw(k: i32): boolean { return tt == T_NAME && tk == k; } function eat(t: i32): boolean { if (tt != t) return false; return nx(); } // Keyword types that take no type arguments (TypeScript parses them as keyword nodes). function isKeywordType(k: i32): boolean { return keywordIn(k, KS_KEYWORD_TYPE) || keywordInHi(k, KS_KEYWORD_TYPE_HI); } function isPropertyNameStart(): boolean { return tokenIn(TS_PROPERTY_NAME); } // Type: union-or-higher, then an optional conditional tail. export function scanType(): boolean { if (!scanUnion()) return false; if (noCond == 0 && kw(K_EXTENDS) && tnl == 0) { if (!nx()) return false; noCond = 1; let ok = scanUnion(); noCond = 0; if (!ok) return false; if (!eat(T_QUESTION)) return false; if (!scanType()) return false; if (!eat(T_COLON)) return false; return scanType(); } return true; } // A type with conditionals re-allowed (inside brackets, parens, braces and type arguments). function scanNested(): boolean { let saved = noCond; noCond = 0; let ok = scanType(); noCond = saved; return ok; } function scanUnion(): boolean { if (is(T_BAR)) { if (!nx()) return false; } if (!scanIntersection()) return false; while (is(T_BAR)) { if (!nx()) return false; if (!scanIntersection()) return false; } return true; } function scanIntersection(): boolean { if (is(T_AMP)) { if (!nx()) return false; } if (!scanOperator()) return false; while (is(T_AMP)) { if (!nx()) return false; if (!scanOperator()) return false; } return true; } function scanOperator(): boolean { if (tt == T_NAME) { if (tk == K_KEYOF || tk == K_UNIQUE || tk == K_READONLY) { if (!nx()) return false; return scanOperator(); } if (tk == K_INFER) { if (!nx()) return false; if (tt != T_NAME) return false; if (!nx()) return false; // `infer U extends C` keeps the constraint unless a `?` follows it where a conditional may start, // in which case the `extends` belongs to the enclosing conditional type. if (kw(K_EXTENDS) && tnl == 0) { let m = tb; if (!nx()) return false; let saved = noCond; noCond = 1; let ok = scanUnion(); noCond = saved; if (!ok) return false; if (noCond == 0 && tt == T_QUESTION) { reseek(m); return more == 0; } } return true; } } return scanPostfix(); } function scanPostfix(): boolean { let saved = noCond; noCond = 0; let ok = scanPrimary(); noCond = saved; if (!ok) return false; while (tnl == 0) { if (is(T_BANG)) { if (!nx()) return false; continue; } if (is(T_QUESTION)) { // JSDoc `T?`, unless a type follows and this is a conditional's `?`. let m = tb; scan(); if (more == 1) return false; let cond = isTypeStart(); reseek(m); if (more == 1 || cond) return more == 0; if (!nx()) return false; continue; } if (!is(T_LBRACK)) break; if (!nx()) return false; if (is(T_RBRACK)) { if (!nx()) return false; } else { if (!scanNested()) return false; if (!eat(T_RBRACK)) return false; } } return true; } // TypeScript's isStartOfType, without its lookaheads for `(` and `-`. function isTypeStart(): boolean { if (tt == T_NAME) { let k = tk; return k == 0 || k >= K_FIRST_CONTEXTUAL || keywordIn(k, KS_TYPE_START); } return tokenIn(TS_TYPE_START); } // `A.B.C`, each segment any name. function scanEntityName(): boolean { if (tt != T_NAME) return false; if (!nx()) return false; return scanQualifiers(); } // `.B.C` after a name; JSDoc's `Array.` stops at the `<`. function scanQualifiers(): boolean { while (is(T_DOT)) { if (!nx()) return false; if (is(T_LT)) return true; if (tt != T_NAME && tt != T_PRIVATE) return false; if (!nx()) return false; } return true; } export function scanTypeArgs(): boolean { if (is(T_SHL)) { rescanLess(); if (more == 1) return false; } if (!eat(T_LT)) return false; if (is(T_GT)) return nx(); while (true) { if (!scanNested()) return false; if (is(T_COMMA)) { if (!nx()) return false; if (is(T_GT)) break; continue; } break; } return eat(T_GT); } function scanOptionalTypeArgs(): boolean { if ((is(T_LT) || is(T_SHL)) && tnl == 0) return scanTypeArgs(); return true; } function scanPrimary(): boolean { if (tt == T_NAME) { let k = tk; if (k == K_TYPEOF) { if (!nx()) return false; if (kw(K_IMPORT)) { if (!scanImportType()) return false; } else if (tt == T_NAME || tt == T_PRIVATE) { if (!scanEntityName()) return false; } else { return false; } return scanOptionalTypeArgs(); } if (k == K_IMPORT) return scanImportType(); if (k == K_ASSERTS) { // `asserts x [is T]` parses in any type position (an error outside a return type). let m = tb; if (!nx()) return false; if (tt == T_NAME && tnl == 0) { if (!nx()) return false; if (!kw(K_IS) || tnl == 1) return true; if (!nx()) return false; return scanType(); } reseek(m); if (more == 1) return false; } if (k == K_FUNCTION) { // JSDoc `function(A, B): R` (an error in TypeScript files). if (!nx()) return false; if (!is(T_LPAREN)) return scanQualifiers(); if (!skipPattern()) return false; if (!is(T_COLON)) return true; if (!nx()) return false; return scanType(); } if (k == K_NEW) return scanFunctionType(); if (k == K_ABSTRACT) { if (!nx()) return false; if (!kw(K_NEW)) return false; return scanFunctionType(); } if (isKeywordType(k)) { if (!nx()) return false; // TypeScript's parseKeywordAndNoDot: `string.X` is a type reference. if (!is(T_DOT) || keywordIn(k, KS_NO_DOT_TYPE)) return true; if (!scanQualifiers()) return false; return scanOptionalTypeArgs(); } if (!scanEntityName()) return false; // TypeScript takes `this is T` in any type position (a grammar error outside a return type). if (k == K_THIS && kw(K_IS) && tnl == 0) { if (!nx()) return false; return scanType(); } return scanOptionalTypeArgs(); } if (tokenIn(TS_STR_NUM_TPL)) return nx(); if (tt == T_MINUS) { if (!nx()) return false; if (tt != T_NUM) return false; return nx(); } if (tt == T_TEMPLATE_HEAD) return scanTemplateType(); if (tt == T_LPAREN) return scanParenOrFunction(); if (tt == T_LT) return scanFunctionType(); if (tt == T_LBRACK) return scanTuple(); if (tt == T_LBRACE) return scanObjectType(); // JSDoc types (errors in TypeScript files): `*`, `?`, `?T`, `!T`. if (tt == T_STAR) return nx(); if (tt == T_QUESTION) { if (!nx()) return false; if (tokenIn(TS_JSDOC_NULLABLE_END)) return true; return scanType(); } if (tt == T_BANG) { if (!nx()) return false; return scanPrimary(); } return false; } function scanImportType(): boolean { if (!nx()) return false; if (!eat(T_LPAREN)) return false; if (!scanNested()) return false; if (is(T_COMMA)) { if (!nx()) return false; if (is(T_LBRACE)) { if (!scanObjectType()) return false; } if (is(T_COMMA)) { if (!nx()) return false; } } if (!eat(T_RPAREN)) return false; while (is(T_DOT)) { if (!nx()) return false; if (tt != T_NAME) return false; if (!nx()) return false; } return scanOptionalTypeArgs(); } function scanTemplateType(): boolean { if (!nx()) return false; while (true) { if (!scanNested()) return false; if (tt != T_RBRACE) return false; rescanTemplate(); if (more == 1) return false; if (tt == T_TEMPLATE_TAIL) return nx(); if (tt != T_TEMPLATE_MID) return false; if (!nx()) return false; } } // `(a: A) => R`, `new (a: A) => R`, `abstract new () => R`. function scanFunctionType(): boolean { if (kw(K_NEW)) { if (!nx()) return false; } if (is(T_LT)) { if (!scanTypeParams()) return false; } if (!is(T_LPAREN)) return false; if (!scanParams()) return false; if (!eat(T_ARROW)) return false; return scanReturnType(); } // Parenthesized type or function type, decided the way TypeScript does (isUnambiguouslyStartOfFunctionType). function scanParenOrFunction(): boolean { if (isFunctionTypeStart()) { if (!scanParams()) return false; if (!eat(T_ARROW)) return false; return scanReturnType(); } if (!nx()) return false; if (!scanNested()) return false; return eat(T_RPAREN); } // At `(`: `()`, `(...`, `(name:`/`,`/`?`/`=`, `(name) =>`, or a binding pattern followed by one of those. function isFunctionTypeStart(): boolean { let m = tb; scan(); var r = false; if (more == 0) { // TypeScript's skipParameterStart: modifiers, then a name or binding pattern. while (tt == T_NAME && isParamModifier(tk)) { let m2 = tb; scan(); if (more == 1) return false; if (!(tokenIn(TS_BINDING_START))) { reseek(m2); break; } } if (tt == T_RPAREN || tt == T_ELLIPSIS) { r = true; } else { var skipped = false; if (tt == T_NAME && (tk == 0 || tk >= K_FIRST_CONTEXTUAL || tk == K_THIS)) { scan(); skipped = more == 0; } else if (tt == T_LBRACK || tt == T_LBRACE) { skipped = skipPattern(); } if (skipped) { if (tokenIn(TS_PARAM_FOLLOW)) { r = true; } else if (tt == T_RPAREN) { scan(); r = more == 0 && tt == T_ARROW; } } } } if (more == 1) return false; reseek(m); return more == 0 && r; } // Skips a bracketed binding pattern from its opener; the lexer ends on the token after it. function skipPattern(): boolean { var depth = 0; while (true) { if (tokenIn(TS_OPEN)) depth = depth + 1; if (tokenIn(TS_CLOSE)) depth = depth - 1; typeEnd = te; scan(); if (more == 1) return false; if (depth == 0) return true; // An unclosed pattern at the end of the input: the lexer stays on T_EOF. if (tt == T_TEMPLATE_HEAD || tt == T_EOF) return false; } } // TypeScript's isModifierKind, which parseModifiers takes before a parameter or type parameter. function isParamModifier(k: i32): boolean { return keywordIn(k, KS_STMT_MOD) || k == K_IN || k == K_OUT; } // `(` parameters `)`: modifiers (errors in a type, but parsed), `...`, a name or binding pattern, `?`, `: T`, and an // initializer (also an error), which is an expression. function scanParams(): boolean { if (!eat(T_LPAREN)) return false; while (!is(T_RPAREN)) { while (tt == T_NAME && isParamModifier(tk)) { let m = tb; if (!nx()) return false; if (!(tokenIn(TS_PARAM_START))) { reseek(m); break; } } if (is(T_ELLIPSIS)) { if (!nx()) return false; } if (tt == T_LBRACK || tt == T_LBRACE) { if (!skipPattern()) return false; } else if (!scanNested()) { return false; } if (is(T_QUESTION)) { if (!nx()) return false; } if (is(T_COLON)) { if (!nx()) return false; if (!scanNested()) return false; } if (is(T_EQ)) { if (!nx()) return false; while (!is(T_COMMA) && !is(T_RPAREN)) { if (tokenIn(TS_OPEN)) { if (!skipPattern()) return false; } else if (tt == T_EOF || tt == T_RBRACK || tt == T_RBRACE || tt == T_TEMPLATE_HEAD) { return false; } else if (!nx()) { return false; } } } if (is(T_COMMA)) { if (!nx()) return false; continue; } if (!is(T_RPAREN)) return false; } return nx(); } // Return type: `asserts x is T`, `asserts this`, `x is T`, `this is T`, or a type. export function scanReturnType(): boolean { if (kw(K_ASSERTS) && tnl == 0) { let save = ts; if (!nx()) return false; if (tt == T_NAME && tnl == 0) { if (!nx()) return false; if (kw(K_IS) && tnl == 0) { if (!nx()) return false; return scanType(); } return true; } // `asserts` on its own is a type reference named asserts. return scanTypeAfterName(); } if (tt == T_NAME) { if (!scanUnion()) return false; if (kw(K_IS) && tnl == 0) { if (!nx()) return false; return scanType(); } return scanConditionalTail(); } return scanType(); } // After scanUnion consumed the first part of a type, finish a possible conditional. function scanConditionalTail(): boolean { if (noCond == 0 && kw(K_EXTENDS) && tnl == 0) { if (!nx()) return false; noCond = 1; let ok = scanUnion(); noCond = 0; if (!ok) return false; if (!eat(T_QUESTION)) return false; if (!scanType()) return false; if (!eat(T_COLON)) return false; return scanType(); } return true; } function scanTypeAfterName(): boolean { // The name was consumed already; continue as a reference with possible type args, then the rest. while (is(T_DOT)) { if (!nx()) return false; if (tt != T_NAME) return false; if (!nx()) return false; } if (!scanOptionalTypeArgs()) return false; while (is(T_LBRACK) && tnl == 0) { if (!nx()) return false; if (!is(T_RBRACK)) { if (!scanNested()) return false; } if (!eat(T_RBRACK)) return false; } while (is(T_BAR) || is(T_AMP)) { if (!nx()) return false; if (!scanOperator()) return false; } return scanConditionalTail(); } function scanTuple(): boolean { if (!eat(T_LBRACK)) return false; while (!is(T_RBRACK)) { if (is(T_ELLIPSIS)) { if (!nx()) return false; } if (!scanNested()) return false; if (is(T_QUESTION)) { if (!nx()) return false; } if (is(T_COLON)) { // `name: T` or `name?: T` member; TypeScript parses `name: ...T` and `name: T?` too (errors). if (!nx()) return false; if (is(T_ELLIPSIS)) { if (!nx()) return false; } if (!scanNested()) return false; if (is(T_QUESTION)) { if (!nx()) return false; } } if (is(T_COMMA)) { if (!nx()) return false; continue; } if (!is(T_RBRACK)) return false; } return nx(); } // `<` params `>`: `[const] [in] [out] Name [extends T] [= T]`. export function scanTypeParams(): boolean { if (!eat(T_LT)) return false; while (!is(T_GT)) { // TypeScript's parseModifiers(permitConst): `const`, `in`, `out`, and the other modifiers (errors). while (tt == T_NAME && isParamModifier(tk)) { let m = tb; if (!nx()) return false; // `` or `` as the parameter's own name. if (tt != T_NAME || tnl == 1 || kw(K_EXTENDS)) { reseek(m); if (more == 1) return false; break; } } if (tt != T_NAME) return false; if (!nx()) return false; if (kw(K_EXTENDS)) { if (!nx()) return false; if (!scanNested()) return false; } if (is(T_EQ)) { if (!nx()) return false; if (!scanNested()) return false; } if (is(T_COMMA)) { if (!nx()) return false; continue; } if (!is(T_GT)) return false; } return nx(); } // Comma-separated heritage elements, after `implements` or `interface X extends`: TypeScript parses each as a // left-hand-side expression (calls and `?.` included, both errors) with type arguments. export function scanTypeList(): boolean { while (true) { if (tt == T_LPAREN || tt == T_LBRACK) { if (!skipPattern()) return false; } else if (!scanEntityName()) { return false; } while (true) { if (is(T_DOT) || is(T_QDOT)) { if (!nx()) return false; if (tt != T_NAME && tt != T_PRIVATE && !is(T_LPAREN) && !is(T_LBRACK)) return false; if (tt == T_NAME || tt == T_PRIVATE) { if (!nx()) return false; } } else if (is(T_LPAREN) || is(T_LBRACK)) { if (!skipPattern()) return false; } else if (is(T_BANG)) { if (!nx()) return false; } else if ((is(T_LT) || is(T_SHL)) && tnl == 0) { if (!scanTypeArgs()) return false; } else { break; } } if (is(T_COMMA)) { if (!nx()) return false; // A trailing comma (an error) before the body or the next clause. if (is(T_LBRACE) || kw(K_EXTENDS) || kw(K_IMPLEMENTS)) return true; continue; } return true; } } function scanMemberSeparator(): boolean { if (is(T_SEMI) || is(T_COMMA)) return nx(); return true; } // `{` members `}`, including mapped types. export function scanObjectType(): boolean { if (!eat(T_LBRACE)) return false; while (!is(T_RBRACE)) { if (!scanTypeMember()) return false; if (!scanMemberSeparator()) return false; } return nx(); } function scanTypeMember(): boolean { // A rest element inside a binding pattern that is being scanned as an object type. if (is(T_ELLIPSIS)) { if (!nx()) return false; if (tt != T_NAME) return false; return nx(); } // Mapped type modifiers: `+readonly`, `-readonly`. if (is(T_PLUS) || is(T_MINUS)) { if (!nx()) return false; if (!kw(K_READONLY)) return false; if (!nx()) return false; return scanIndexOrMapped(); } // Modifiers: readonly, accessibility, static, declare, accessor, get, set. Each only when a name follows. while (tt == T_NAME) { let k = tk; let isMod = keywordIn(k, KS_TYPE_MEMBER_MOD); if (!isMod) break; let save = ts; if (!nx()) return false; if (!(isPropertyNameStart() && tnl == 0)) { // The keyword was the member's own name; its suffix follows. return scanMemberAfterName(); } } if (is(T_LBRACK)) return scanIndexOrMapped(); if (is(T_LPAREN) || is(T_LT)) return scanSignatureRest(); if (kw(K_NEW)) { if (!nx()) return false; if (is(T_LPAREN) || is(T_LT)) return scanSignatureRest(); return scanMemberAfterName(); } if (tokenIn(TS_MEMBER_NAME)) { if (!nx()) return false; return scanMemberAfterName(); } return false; } // After a member name: `?`, then `(`/`<` signature or `: T`, or nothing. TypeScript also parses an accessor's // body and a property's initializer here (errors). function scanMemberAfterName(): boolean { if (is(T_QUESTION)) { if (!nx()) return false; } if (is(T_LPAREN) || is(T_LT)) { if (!scanSignatureRest()) return false; if (is(T_LBRACE)) return skipPattern(); return true; } if (is(T_COLON)) { if (!nx()) return false; if (!scanNested()) return false; } if (is(T_EQ)) { if (!nx()) return false; var first = true; while (!is(T_SEMI) && !is(T_COMMA) && !is(T_RBRACE) && !is(T_EOF) && (first || tnl == 0)) { first = false; if (tokenIn(TS_OPEN)) { if (!skipPattern()) return false; } else if (tt == T_TEMPLATE_HEAD || tt == T_RBRACK || tt == T_RPAREN) { return false; } else if (!nx()) { return false; } } } return true; } // Signature from `<` or `(`: type params, params, optional `: T`. function scanSignatureRest(): boolean { if (is(T_LT)) { if (!scanTypeParams()) return false; } if (!scanParams()) return false; if (is(T_COLON)) { if (!nx()) return false; return scanReturnType(); } return true; } // `[` ... : index signature `[k: T]: U`, mapped type `[K in T as U]: V`, or computed name `[a.b]`; anything else // TypeScript parses there (odd index parameters, a computed name's expression) is skipped as a balanced run. function scanIndexOrMapped(): boolean { let m = tb; if (scanIndexForm()) return true; if (more == 1) return false; reseek(m); if (more == 1 || !skipPattern()) return false; return scanMemberAfterName(); } function scanIndexForm(): boolean { if (!eat(T_LBRACK)) return false; if (tt == T_NAME) { if (!nx()) return false; if (is(T_COLON)) { if (!nx()) return false; if (!scanNested()) return false; if (!eat(T_RBRACK)) return false; return scanMemberAfterName(); } if (kw(K_IN)) { if (!nx()) return false; if (!scanNested()) return false; if (kw(K_AS)) { if (!nx()) return false; if (!scanNested()) return false; } if (!eat(T_RBRACK)) return false; if (is(T_PLUS) || is(T_MINUS)) { if (!nx()) return false; } if (is(T_QUESTION)) { if (!nx()) return false; } if (is(T_COLON)) { if (!nx()) return false; if (!scanNested()) return false; } return true; } // computed name `[Symbol.iterator]` while (is(T_DOT)) { if (!nx()) return false; if (tt != T_NAME) return false; if (!nx()) return false; } if (!eat(T_RBRACK)) return false; return scanMemberAfterName(); } if (tokenIn(TS_STR_NUM_TPL)) { if (!nx()) return false; if (!eat(T_RBRACK)) return false; return scanMemberAfterName(); } return false; }