Skip to content

Commit c25f8b2

Browse files
committed
docs: record as contextual-keyword fix as deferred (entangled)
Attempted to generalize the #891 `from` guard to `as`/`keyof`/`is`/`infer`/`satisfies` (all 'keyword only before a type/value'). It failed and was reverted: unlike `from` (one scoping mechanism), these are scoped by four at once — the flat keyword group, a per-keyword `keyword.operator.expression.as` rule, the type-inner injection, and the structural type rules. De-keywording from the flat group alone (a) didn't fix `const as = 1` and (b) regressed `keyof`/`is`/`infer` in type position to entity.name.type (coverage 99.3%->99.2%). A correct fix must reconcile all four. No functional change: gen-tm reverted to the `from`-only guard (tmLanguage byte-identical), plus a clarifying comment; ledger documents the attempt + finding honestly. Coverage 99.3%, test-issues 313/0 unchanged.
1 parent a5fdab6 commit c25f8b2

2 files changed

Lines changed: 12 additions & 8 deletions

File tree

docs/upstream-issues.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@ TM-expressible parse/lex disambiguation in Monogram's wheelhouse, still open aft
5656
- `#1071` `import.meta.dirname`**contested:** we scope `import` as `keyword.control.import` (defensible — it *is* the import meta-property keyword) and `meta`/`dirname` as properties, with no breakage; the reporter wants `import` non-keyworded. Left until the intended scope is settled.
5757
- `#1048` multiline `extends` in a generic with split parens; `#857` multiple `export default` overloads; `#497` `import` declaration / semicolon scoping; `#810` trailing newline scope in a line comment; `#1039` `String.raw` escape-token visibility.
5858

59-
**Contextual keywords (the `#891` class).** A word that is a keyword in some positions but a valid identifier elsewhere (`from`, `as`, `of`, …) was mis-scoped as a keyword *everywhere* by the flat global keyword match. Fixed generically for the always-before-a-fixed-token case: a scope-keyword the grammar *always* places immediately before the string token (only `from`: `'from' String_`) is now emitted with a `(?=\s*["'])` lookahead, so `const from = 1` / `from()` / the `import from from "x"` binding scope as `variable`/`entity.name.function` (matching the official grammar) while the import-source `from` stays a keyword. **Still open:** `as` as a plain identifier (`const as = 1`) — official scopes it `variable`, we still over-scope it (it's entangled with the `expr as Type` cast and the `x is T` type-predicate rules); `of` as an identifier (`const of = 1`) — **the official grammar mis-scopes this too**, so it's a both-wrong case, not a gap. Closing `as` needs the same positional treatment the cast/predicate rules already use.
59+
**Contextual keywords (the `#891` class).** A word that is a keyword in some positions but a valid identifier elsewhere (`from`, `as`, `of`, …) was mis-scoped as a keyword *everywhere* by the flat global keyword match. **Fixed** for the always-before-a-fixed-token case: a scope-keyword the grammar *always* places immediately before the string token (only `from`: `'from' String_`) is emitted with a `(?=\s*["'])` lookahead, so `const from = 1` / `from()` / the `import from from "x"` binding scope as `variable`/`entity.name.function` (matching official) while the import-source `from` stays a keyword.
60+
61+
**Still open — `as` (attempted, reverted, deferred).** Generalizing the `from` guard to `as`/`keyof`/`is`/`infer`/`satisfies` (all "keyword only before a type/value") *failed*: unlike `from`, these are scoped by **four mechanisms at once** — the flat keyword group, a per-keyword `keyword.operator.expression.as` rule, the `type-inner` injection, and the structural type rules. De-keywording from the flat group alone both (a) *didn't* fix `const as = 1` (the per-keyword rule still fired) and (b) regressed `keyof`/`is`/`infer` in type position to `entity.name.type` (coverage 99.3%→99.2%). A correct fix must reconcile all four mechanisms positionally — real work, deferred. `of` as an identifier (`const of = 1`) — **the official grammar mis-scopes it too**, so it's a both-wrong case, not a gap.
6062

6163
> Action: run each through `test/test-issues.ts` to confirm before claiming. Listing here ≠ "breaks in Monogram" — it means "not yet correct-and-tested."
6264

src/gen-tm.ts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,13 +2260,15 @@ export function generateTmLanguage(grammar: CstGrammar, langName: string): TmGra
22602260
keywordGroups.get(scope)!.push(ident);
22612261
}
22622262
}
2263-
// A contextual keyword that the grammar ALWAYS places immediately before the
2264-
// string token (e.g. `'from' String_` in every import/export rule) is a keyword
2265-
// ONLY in that position — everywhere else it is a plain identifier (`const from = 1`,
2266-
// `from()`). Matching it globally mis-scopes those identifier uses, so emit it with a
2267-
// lookahead for a following string literal and let other uses fall through to
2268-
// identifier scoping. Structural + agnostic: keyed on "always-before-the-string-token",
2269-
// never on the word itself.
2263+
// A contextual keyword that the grammar ALWAYS places immediately before the string
2264+
// token (e.g. `'from' String_` in every import/export rule) is a keyword ONLY in that
2265+
// position — everywhere else it is a plain identifier (`const from = 1`, `from()`).
2266+
// Matching it globally mis-scopes those identifier uses, so emit it with a lookahead
2267+
// for a following string literal and let other uses fall through to identifier scoping.
2268+
// Structural + agnostic: keyed on "always-before-the-string-token", never on the word.
2269+
// (Only `from` qualifies. Other contextual keywords — `as`, `keyof`, `is`, … — are
2270+
// scoped by several mechanisms at once here, NOT just this flat group, so they can't be
2271+
// de-keyworded from one place; see docs/upstream-issues.md.)
22702272
const stringTokName = grammar.tokens.find(t => t.string)?.name;
22712273
const alwaysBeforeString = (lit: string): boolean => {
22722274
if (!stringTokName) return false;

0 commit comments

Comments
 (0)