From 336cfc5389dc43fd21a41e9e7ef1315d64c17843 Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Thu, 22 Oct 2026 15:51:24 +1100 Subject: [PATCH 1/2] refactor(kirari): describe function syntax per language Move per-language function detection out of definitions.ts into a FunctionSyntax object in each language file. classifyDefinitions is now language-agnostic and reads declaration keywords, literal forms, annotations, and call styles from that object. Output is unchanged. The language-specific objects add about 200 bytes gzip to the all-languages bundle, so its budget rises from 23,200 to 23,400 bytes. Core and JavaScript bundles are unchanged. --- packages/kirari/scripts/size.ts | 2 +- packages/kirari/src/grammars/definitions.ts | 307 ++++++++++-------- .../kirari/src/grammars/definitions/state.ts | 3 +- packages/kirari/src/languages/c.ts | 2 +- packages/kirari/src/languages/clojure.ts | 22 +- packages/kirari/src/languages/cpp.ts | 16 +- packages/kirari/src/languages/csharp.ts | 2 +- packages/kirari/src/languages/dart.ts | 2 +- packages/kirari/src/languages/elixir.ts | 12 +- packages/kirari/src/languages/glsl.ts | 2 +- packages/kirari/src/languages/go.ts | 12 +- packages/kirari/src/languages/groovy.ts | 4 +- packages/kirari/src/languages/java.ts | 2 +- packages/kirari/src/languages/julia.ts | 2 +- packages/kirari/src/languages/kotlin.ts | 4 +- packages/kirari/src/languages/lua.ts | 2 +- packages/kirari/src/languages/php.ts | 2 +- packages/kirari/src/languages/powershell.ts | 4 +- packages/kirari/src/languages/protobuf.ts | 2 +- packages/kirari/src/languages/python.ts | 12 +- packages/kirari/src/languages/r.ts | 2 +- packages/kirari/src/languages/ruby.ts | 19 +- packages/kirari/src/languages/rust.ts | 6 +- packages/kirari/src/languages/scala.ts | 18 +- packages/kirari/src/languages/shell.ts | 2 +- packages/kirari/src/languages/sql.ts | 2 +- packages/kirari/src/languages/swift.ts | 4 +- packages/kirari/src/languages/zig.ts | 2 +- packages/kirari/src/stream/resumable.ts | 4 +- 29 files changed, 306 insertions(+), 169 deletions(-) diff --git a/packages/kirari/scripts/size.ts b/packages/kirari/scripts/size.ts index b8ccf76..3f93ecc 100644 --- a/packages/kirari/scripts/size.ts +++ b/packages/kirari/scripts/size.ts @@ -10,7 +10,7 @@ const entries = { const budgets: Record = { core: 3_900, javascript: 4_400, - 'all languages': 23_200, + 'all languages': 23_400, } const over: string[] = [] diff --git a/packages/kirari/src/grammars/definitions.ts b/packages/kirari/src/grammars/definitions.ts index c6032a5..a93eb43 100644 --- a/packages/kirari/src/grammars/definitions.ts +++ b/packages/kirari/src/grammars/definitions.ts @@ -1,28 +1,78 @@ +// Function classification for languages without a dedicated scanner pass. +// +// After a language is lexed, `classifyDefinitions` visits each name and marks it as a function +// when it is declared (`def name`), called (`name(`), or bound to a function literal +// (`name = fn …`). The algorithm is shared; each language describes how it writes functions +// with a `FunctionSyntax` in its own definition file. import { isSignificant } from '../types.js' import type { Token } from '../types.js' -const declarations: Record = { - c: null, cpp: null, csharp: null, dart: null, glsl: null, java: null, groovy: null, - clojure: /^(defn-?|defmacro|defmulti|defmethod|fn\*?)$/, - elixir: /^(defp?|defmacro[p]?|defguard[p]?|defdelegate)$/, - go: /^func$/, julia: /^(function|macro)$/, kotlin: /^fun$/, lua: /^function$/, - php: /^function$/i, powershell: /^(function|filter|workflow)$/i, python: /^def$/, - r: null, ruby: /^def$/, rust: /^fn$/, scala: /^def$/, shell: /^function$/, - swift: /^func$/, zig: /^fn$/, sql: /^(function|procedure)$/i, protobuf: /^rpc$/, -} +export const pairs: Record = { '(': ')', '[': ']', '{': '}', '#(': ')', '@(': ')', '@{': '}' } +export const memberAccess = /^(\.|\?\.|->|\?->|::)$/ +const qualifier = /^(\.|:)$/ -const literals: Record = { - csharp: /^delegate$/, elixir: /^fn$/, go: /^func$/, julia: /^function$/, - kotlin: /^fun$/, lua: /^function$/, php: /^(function|fn)$/i, - python: /^lambda$/, r: /^(function|\\)$/, ruby: /^(lambda|proc)$/, +/** Bracket structure of the significant tokens being classified, for `FunctionSyntax` hooks. */ +export type Structure = { + /** Value of the significant token at `index`, or `''` outside the tokens. */ + value(index: number): string + /** Index of the bracket closing the one that opens at `index`. */ + closing(index: number): number | undefined + /** Index of the innermost bracket enclosing `index`; `-1` for one opened before these tokens. */ + parent(index: number): number | undefined + /** Whether `index` opens a bracket that does not close within these tokens. */ + unclosed(index: number): boolean + /** Whether the group opening at `start` is a body, not a value that is then called or indexed. */ + block(start: number): boolean } -// Modifiers that may precede a function literal, such as `async move |x|` in Rust. -const modifiers: Record = { - csharp: /^(async|static)$/, php: /^static$/i, rust: /^(async|move)$/, +/** How a language writes functions. Everything is optional; an empty syntax detects only calls. */ +export type FunctionSyntax = { + /** Keyword before a declared name: `def` in `def name`. */ + readonly declaration?: RegExp + /** Keyword that starts a function literal: `lambda`, `func`, `fun`. */ + readonly literal?: RegExp + /** Token required right after the `literal` keyword: Ruby `lambda {` and `proc do`. */ + readonly literalOpener?: RegExp + /** A literal's body must not be invoked at once: `func () { … }()` is a call result, not a function. */ + readonly literalBody?: boolean + /** Keywords whose `{ … }` is a type body, not a literal body: Go `struct` and `interface`. */ + readonly typeBodies?: RegExp + /** Modifiers that may precede a literal: Rust `async move |x|`. */ + readonly modifiers?: RegExp + /** Arrow of lambdas such as `(x) => …` and `x -> …`. */ + readonly arrow?: '=>' | '->' + /** Type parameters may precede lambda parameters: Dart `(x) => …`. */ + readonly typeParameters?: boolean + /** Lambda parameters may be followed by a modifier and a block body: Dart `(x) async { … }`. */ + readonly parameterBodies?: boolean + /** Other function literal forms; returns `undefined` when `first` starts none of them. */ + readonly forms?: (first: string, start: number, limit: number, structure: Structure) => boolean | undefined + /** `<` and `>` are always comparison operators, never generic brackets: Python. */ + readonly comparisonAngles?: boolean + /** Generic calls such as `name(…)`. */ + readonly genericCalls?: boolean + /** Calls are written `(name …)`, so `name(` is not one: Clojure. */ + readonly prefixCalls?: boolean + /** Names with a `$` or `@` sigil can be called: C# verbatim identifiers such as `@class(`. */ + readonly sigilCalls?: boolean + /** Bindings may be annotated: `name: T = …`. */ + readonly annotations?: boolean + /** Variable declarations may be annotated: Go `var name T = …`. */ + readonly varAnnotations?: boolean + /** An annotation outside brackets ends at a line break: Python. */ + readonly lineAnnotations?: boolean + /** Assignment operators for bindings. Defaults to `=`. */ + readonly assignment?: RegExp + /** Other positions that bind the name at `index` to the following value: Clojure `(def name …)`. */ + readonly binding?: (index: number, structure: Structure) => boolean + /** A decorator name at the start of a logical line is a function: Python `@name`. */ + readonly decorators?: boolean } -export const pairs: Record = { '(': ')', '[': ']', '{': '}', '#(': ')', '@(': ')', '@{': '}' } +/** `{ … }` closures that are not invoked at once, as in Kotlin and Swift. */ +export const braceClosure: NonNullable = (first, start, _limit, structure) => { + return first === '{' ? structure.block(start) : undefined +} /** * Indexes bracket structure over significant tokens: `closes` maps an opening index to its @@ -61,20 +111,15 @@ const matchBrackets = (parts: readonly Token[], open: readonly string[], generic } // Classify after grammar-specific lexing so quoted names work and opaque literals stay opaque. -// -// Works on significant tokens only (`parts`) in two steps: index bracket structure, then visit -// each name and mark it as a function when it is declared (`def name`), called (`name(`), or -// bound to a function literal (`name = fn …`). Streams pass `open` when `tokens` start inside brackets. -export const classifyDefinitions = (tokens: Token[], language: string, open: readonly string[] = []): Token[] => { - if (!Object.hasOwn(declarations, language)) return tokens - const declaration = declarations[language] +// Streams pass `open` when `tokens` start inside brackets. +export const classifyDefinitions = (tokens: Token[], syntax: FunctionSyntax, open: readonly string[] = []): Token[] => { const positions: number[] = [] const parts = tokens.filter((token, index) => { if (!isSignificant(token.type)) return false positions.push(index) return true }) - const { closes, opens, parents } = matchBrackets(parts, open, language !== 'python') + const { closes, opens, parents } = matchBrackets(parts, open, !syntax.comparisonAngles) const lineBreakBefore = (index: number): boolean => { for (let at = positions[index - 1]! + 1; at < positions[index]!; at++) if (endsLine(tokens[at]!.value)) return true return false @@ -85,96 +130,75 @@ export const classifyDefinitions = (tokens: Token[], language: string, open: rea const end = closes.get(start) return end === undefined || !invoked(end) } - const arrow = language === 'java' || language === 'julia' ? '->' : '=>' - const arrows = /^(csharp|dart|java|julia|scala)$/.test(language) - - // Language-specific function literal forms; undefined when `first` starts none of them. - const specialForm = (first: string, start: number, limit: number): boolean | undefined => { - switch (language) { - case 'clojure': return first === '#(' || first === '(' && /^fn\*?$/.test(value(start + 1)) - case 'rust': return first === '|' || first === '||' ? true : undefined - case 'elixir': return first === '&' ? true : undefined - case 'ruby': { - if (first !== '->') return undefined - let body = start + 1 - if (value(body) === '(') body = (closes.get(body) ?? limit) + 1 - return value(body) === 'do' || value(body) === '{' && block(body) - } - case 'groovy': case 'kotlin': case 'powershell': case 'swift': - return first === '{' ? block(start) : undefined - case 'scala': { - // `{ case … => … }` and `{ x => … }` - if (first !== '{') return undefined - for (let cursor = start + 1; cursor < (closes.get(start) ?? limit); cursor++) { - if (value(cursor) === '=>') return block(start) - if (pairs[value(cursor)] && !closes.has(cursor)) return false - cursor = closes.get(cursor) ?? cursor - } - return false - } - case 'cpp': { - // `[captures](params) specifiers -> type { … }` - if (first !== '[') return undefined - let cursor = (closes.get(start) ?? limit) + 1 - while (cursor < limit) { - if (value(cursor) === '{') return block(cursor) - if (/^[;=}]$/.test(value(cursor))) return false - if (pairs[value(cursor)] && !closes.has(cursor)) return false - cursor = (closes.get(cursor) ?? cursor) + 1 - } - return false - } - } - return undefined + const unclosed = (index: number): boolean => !!pairs[value(index)] && !closes.has(index) + const structure: Structure = { + value, + closing: index => closes.get(index), + parent: index => parents.get(index), + unclosed, + block, } + const { arrow } = syntax // A literal introduced by a keyword such as `func`, `fun`, `lambda`, or `delegate`. const keywordLiteral = (start: number, limit: number): boolean => { - if (language === 'ruby' && !/^(\{|do|\()$/.test(value(start + 1))) return false + if (syntax.literalOpener && !syntax.literalOpener.test(value(start + 1))) return false // shortcut: recognize explicit literals, not inferred aliases or results of arbitrary expressions. - if (/^(csharp|go|kotlin|php|ruby)$/.test(language)) { - for (let cursor = start + 1; cursor < limit; cursor++) { - if (value(cursor) === '{' && !(language === 'go' && /^(struct|interface)$/.test(value(cursor - 1)))) return block(cursor) - if (/^(;|=|:=|=>|do|end)$/.test(value(cursor))) break - if (pairs[value(cursor)] && !closes.has(cursor)) return true - cursor = closes.get(cursor) ?? cursor - } + if (!syntax.literalBody) return true + for (let cursor = start + 1; cursor < limit; cursor++) { + if (value(cursor) === '{' && !syntax.typeBodies?.test(value(cursor - 1))) return block(cursor) + if (/^(;|=|:=|=>|do|end)$/.test(value(cursor))) break + if (unclosed(cursor)) return true + cursor = closes.get(cursor) ?? cursor } return true } + // `(params) => …`, or with parameter bodies `(params) async { … }`. Undefined when parentheses + // that are not invoked may just group the literal, as in `(x => x)`. + const parameters = (end: number): boolean | undefined => { + if (arrow && value(end + 1) === arrow) return true + if (!syntax.parameterBodies) return undefined + let body = end + 1 + if (value(body) === 'async' || value(body) === 'sync') { + body++ + if (value(body) === '*') body++ + } + if (value(body) === '=>') return true + if (value(body) === '{') return block(body) + return undefined + } + // Whether the expression starting at `start` is a function literal. const literal = (start: number, limit = parts.length): boolean => { while (start < limit) { const first = value(start) - if (modifiers[language]?.test(first)) { start++; continue } - const special = specialForm(first, start, limit) - if (special !== undefined) return special - if (literals[language]?.test(first)) return keywordLiteral(start, limit) - // Type parameters: Dart `(x) => …` continues; Scala `[T] => …` decides here. - if ((first === '<' && language === 'dart') || (first === '[' && language === 'scala')) { + if (syntax.modifiers?.test(first)) { + start++ + continue + } + const form = syntax.forms?.(first, start, limit, structure) + if (form !== undefined) return form + if (syntax.literal?.test(first)) return keywordLiteral(start, limit) + if (first === '<' && syntax.typeParameters) { const end = closes.get(start) if (end === undefined) return false - if (language === 'scala') return value(end + 1) === '=>' start = end + 1 continue } if (first === '(') { const end = closes.get(start) if (end === undefined || end >= limit) return false - if (arrows && value(end + 1) === arrow) return true - if (language === 'dart') { - let body = end + 1 - if (value(body) === 'async' || value(body) === 'sync') { body++; if (value(body) === '*') body++ } - if (value(body) === '=>') return true - if (value(body) === '{') return block(body) - } + const lambda = parameters(end) + if (lambda !== undefined) return lambda if (invoked(end)) return false + // Look inside the parentheses. limit = end start++ continue } - return arrows && /^(variable|keyword)$/.test(parts[start]?.type ?? '') && value(start + 1) === arrow + // `x => …` with a single unparenthesized parameter. + return !!arrow && /^(variable|keyword)$/.test(parts[start]?.type ?? '') && value(start + 1) === arrow } return false } @@ -182,64 +206,77 @@ export const classifyDefinitions = (tokens: Token[], language: string, open: rea // Qualified declarations such as `def self.name` or `function obj:name` record each // qualifier so the final segment still counts as declared. const qualifiers = new Set() - for (let index = 0; index < parts.length; index++) { - const token = parts[index]! - if (!/^(variable|property|type|function|keyword)$/.test(token.type)) continue + const declared = (index: number): boolean => { // Find the token before the name, skipping a `*`/`&` marker and type parameters (`fun name`). let before = index - 1 if (value(before) === '*' || value(before) === '&') before-- if (opens.has(before)) before = opens.get(before)! - 1 - const followsDeclaration = !!declaration?.test(value(before)) - || /^(\.|:)$/.test(value(before)) && qualifiers.has(before - 1) + const followsDeclaration = !!syntax.declaration?.test(value(before)) + || qualifier.test(value(before)) && qualifiers.has(before - 1) + if (!followsDeclaration) return false const qualifierEnd = value(index + 1) === '<' ? closes.get(index + 1) ?? index : index - const qualified = followsDeclaration && /^(\.|:)$/.test(value(qualifierEnd + 1)) - if (qualified) qualifiers.add(qualifierEnd) - const declared = followsDeclaration && !qualified - // Decorators start a logical line outside brackets; matrix multiplication does not. - const decorated = language === 'python' && value(index - 1) === '@' && value(index + 1) !== '.' && !lineBreakBefore(index) - && !parents.has(index - 1) && (index === 1 || lineBreakBefore(index - 1)) - let isFunction = declared || decorated - const member = /^(\.|\?\.|->|\?->|::)$/.test(value(index - 1)) + if (!qualifier.test(value(qualifierEnd + 1))) return true + qualifiers.add(qualifierEnd) + return false + } + + // Decorators start a logical line outside brackets; matrix multiplication does not. + const decorated = (index: number): boolean => { + if (!syntax.decorators || value(index - 1) !== '@' || value(index + 1) === '.') return false + const startsLine = index === 1 || lineBreakBefore(index - 1) + return !lineBreakBefore(index) && !parents.has(index - 1) && startsLine + } + + // Calls: `name(` and generic `name(`. + const called = (index: number): boolean => { + const token = parts[index]! + if (syntax.prefixCalls || /^[$@]/.test(token.value) && !syntax.sigilCalls) return false + const next = index + 1 + if (value(next) === '(') return true + if (value(next) !== '<' || !syntax.genericCalls) return false + const adjacent = parts[next]?.start === token.end || /^(type|variable)$/.test(parts[index - 1]?.type ?? '') + return adjacent && value((closes.get(next) ?? parts.length) + 1) === '(' + } + + // The index of the `=` after a type annotation, or of where the annotation scan stopped. + const skipAnnotation = (index: number): number => { let next = index + 1 - if (!declared && token.type !== 'variable' && token.type !== 'property') continue - // Calls: `name(` and generic `name(`. - if (!member && language !== 'clojure' && (!/^[$@]/.test(token.value) || language === 'csharp')) { - if (value(next) === '(') isFunction = true - if (value(next) === '<' && /^(cpp|csharp|dart|java|rust|swift)$/.test(language) - && (parts[next]?.start === token.end || /^(type|variable)$/.test(parts[index - 1]?.type ?? '')) - && value((closes.get(next) ?? parts.length) + 1) === '(') isFunction = true + while (++next < parts.length) { + if (syntax.lineAnnotations && !parents.has(index) && lineBreakBefore(next)) return -1 + if (value(next) === '=') break + if (/^[;{},):\]]$/.test(value(next)) || /^(val|var|let|def)$/.test(value(next))) break + if (unclosed(next)) break + next = closes.get(next) ?? next } - // Bindings to function literals, skipping a type annotation (`name: T = …`, Go `var name T = …`). - if (!member) { - if (value(next) === ':' && /^(python|kotlin|scala|swift|rust)$/.test(language) - || language === 'go' && value(index - 1) === 'var' && value(next) !== '=') { - while (++next < parts.length) { - // A top-level Python annotation ends with its logical line. - if (language === 'python' && !parents.has(index) && lineBreakBefore(next)) { next = -1; break } - if (value(next) === '=') break - if (/^[;{},):\]]$/.test(value(next)) || /^(val|var|let|def)$/.test(value(next))) break - if (pairs[value(next)] && !closes.has(next)) break - next = closes.get(next) ?? next - } - } - const assignment = value(next) === '=' || language === 'go' && value(next) === ':=' - || language === 'r' && /^(<-|<<-)$/.test(value(next)) - if (assignment && literal(next + 1)) isFunction = true - if (language === 'clojure') { - const parent = parents.get(index) - const binding = /^(def|defonce)$/.test(value(index - 1)) - || parent !== undefined && value(parent) === '[' && /^(let\*?|loop\*?|binding)$/.test(value(parent - 1)) - if (binding && literal(next)) isFunction = true - } + return next + } + + // Bindings to function literals: `name = fn …`, `name: T = fn …`. + const bound = (index: number): boolean => { + const annotated = syntax.annotations && value(index + 1) === ':' + || syntax.varAnnotations && value(index - 1) === 'var' && value(index + 1) !== '=' + const next = annotated ? skipAnnotation(index) : index + 1 + const assignment = syntax.assignment ? syntax.assignment.test(value(next)) : value(next) === '=' + if (assignment && literal(next + 1)) return true + return !!syntax.binding?.(index, structure) && literal(next) + } + + for (let index = 0; index < parts.length; index++) { + const token = parts[index]! + if (!/^(variable|property|type|function|keyword)$/.test(token.type)) continue + const isDeclared = declared(index) + if (!isDeclared && token.type !== 'variable' && token.type !== 'property') continue + const member = memberAccess.test(value(index - 1)) + if (isDeclared || decorated(index) || !member && (called(index) || bound(index))) { + tokens[positions[index]!] = { ...token, type: 'function' } } - if (isFunction) tokens[positions[index]!] = { ...token, type: 'function' } } return tokens } -/** A `Language.definitions` callback that classifies as the built-in `language`, whatever the definition is named. */ -export const definitionsFor = (language: string) => { - return (tokens: Token[], open?: readonly string[]): Token[] => classifyDefinitions(tokens, language, open) +/** A `Language.definitions` callback that classifies functions written with `syntax`. */ +export const definitionsFor = (syntax: FunctionSyntax) => { + return (tokens: Token[], open?: readonly string[]): Token[] => classifyDefinitions(tokens, syntax, open) } /** Whether text contains a newline that is not a backslash line continuation. */ diff --git a/packages/kirari/src/grammars/definitions/state.ts b/packages/kirari/src/grammars/definitions/state.ts index 75546f8..a982ee4 100644 --- a/packages/kirari/src/grammars/definitions/state.ts +++ b/packages/kirari/src/grammars/definitions/state.ts @@ -1,7 +1,6 @@ import type { Token } from '../../types.js' -import { pairs } from '../definitions.js' +import { memberAccess, pairs } from '../definitions.js' -const memberAccess = /^(\.|\?\.|->|\?->|::)$/ const nameType = /^(variable|property|type|function|keyword|boolean|null)$/ const backReference = /^(def|\*|&|\.|:|\?\.|->|\?->|::|>+)$/ diff --git a/packages/kirari/src/languages/c.ts b/packages/kirari/src/languages/c.ts index 7fd36e3..6d508f5 100644 --- a/packages/kirari/src/languages/c.ts +++ b/packages/kirari/src/languages/c.ts @@ -4,7 +4,7 @@ import { identifier, whitespace, lineComment, blockComment, preprocessor, operat export const c: Language = { name: 'c', - definitions: definitionsFor('c'), + definitions: definitionsFor({}), identifier, keywords: 'alignas alignof auto break case const constexpr continue default do else enum extern for goto if inline register restrict return sizeof static static_assert struct switch thread_local typedef typeof typeof_unqual union volatile while _Alignas _Alignof _Atomic _Generic _Noreturn _Static_assert _Thread_local'.split(' '), types: 'bool char double float int long short signed unsigned void _Bool _Complex _Imaginary'.split(' '), diff --git a/packages/kirari/src/languages/clojure.ts b/packages/kirari/src/languages/clojure.ts index 43ff94a..79deeaf 100644 --- a/packages/kirari/src/languages/clojure.ts +++ b/packages/kirari/src/languages/clojure.ts @@ -1,13 +1,33 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' const symbol = /[^\s,()[\]{}";@^`~\\:#'][^\s,()[\]{}";@^`~\\]*/ const boundary = /(?=[\s,()[\]{}";@^`~\\]|$)/ const keywords = 'def defn defn- defmacro defonce defmulti defmethod defprotocol defrecord deftype fn fn* let let* letfn loop loop* recur if if-let if-not when when-let when-not cond case do quote var set! throw try catch finally new . monitor-enter monitor-exit ns in-ns require import use binding for doseq lazy-seq'.split(' ') +// `#(…)` and `(fn …)`; any other first token is not a literal. +const anonymous: FunctionSyntax['forms'] = (first, start, _limit, { value }) => { + return first === '#(' || first === '(' && /^fn\*?$/.test(value(start + 1)) +} + +// `(def name …)`, `(defonce name …)`, and names in `let`, `loop`, and `binding` vectors. +const binding: FunctionSyntax['binding'] = (index, { value, parent }) => { + const vector = parent(index) + return /^(def|defonce)$/.test(value(index - 1)) + || vector !== undefined && value(vector) === '[' && /^(let\*?|loop\*?|binding)$/.test(value(vector - 1)) +} + +const functions: FunctionSyntax = { + declaration: /^(defn-?|defmacro|defmulti|defmethod|fn\*?)$/, + forms: anonymous, + prefixCalls: true, + binding, +} + export const clojure: Language = { name: 'clojure', - definitions: definitionsFor('clojure'), + definitions: definitionsFor(functions), rules: [ { type: 'text', pattern: /[\s,]+/ }, { type: 'comment', pattern: /;[^\r\n]*|#![^\r\n]*/ }, diff --git a/packages/kirari/src/languages/cpp.ts b/packages/kirari/src/languages/cpp.ts index 35aca56..1e80ac4 100644 --- a/packages/kirari/src/languages/cpp.ts +++ b/packages/kirari/src/languages/cpp.ts @@ -1,11 +1,25 @@ import type { Language } from '../types.js' import { c } from './c.js' import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' + +// Lambdas: `[captures](params) specifiers -> type { … }`. +const lambda: FunctionSyntax['forms'] = (first, start, limit, { value, closing, unclosed, block }) => { + if (first !== '[') return undefined + let cursor = (closing(start) ?? limit) + 1 + while (cursor < limit) { + if (value(cursor) === '{') return block(cursor) + if (/^[;=}]$/.test(value(cursor))) return false + if (unclosed(cursor)) return false + cursor = (closing(cursor) ?? cursor) + 1 + } + return false +} export const cpp: Language = { ...c, name: 'cpp', - definitions: definitionsFor('cpp'), + definitions: definitionsFor({ genericCalls: true, forms: lambda }), keywords: [...c.keywords!, ...'alignas alignof and and_eq asm bitand bitor catch class compl concept consteval constinit const_cast co_await co_return co_yield decltype delete dynamic_cast explicit export friend mutable namespace new noexcept not not_eq operator or or_eq private protected public reinterpret_cast requires static_cast template this thread_local throw try typeid typename using virtual xor xor_eq'.split(' ')], types: [...c.types!, 'char8_t', 'char16_t', 'char32_t', 'wchar_t'], rules: [ diff --git a/packages/kirari/src/languages/csharp.ts b/packages/kirari/src/languages/csharp.ts index 411d070..d34426e 100644 --- a/packages/kirari/src/languages/csharp.ts +++ b/packages/kirari/src/languages/csharp.ts @@ -35,7 +35,7 @@ const rawStrings = (source: string, language: Language): Token[] => { export const csharp: Language = { name: 'csharp', - definitions: definitionsFor('csharp'), + definitions: definitionsFor({ literal: /^delegate$/, literalBody: true, modifiers: /^(async|static)$/, arrow: '=>', genericCalls: true, sigilCalls: true }), tokenize: rawStrings, identifier, keywords: 'abstract add alias and as ascending async await base break by case catch checked class const continue default delegate descending do else enum event explicit extern file finally fixed for foreach from get global goto group if implicit in init interface internal into is join let lock managed nameof namespace new not or orderby out override params partial private protected public readonly record ref remove required return scoped sealed select set sizeof stackalloc static struct switch this throw try typeof unchecked unmanaged unsafe using value var virtual volatile when where while with yield'.split(' '), diff --git a/packages/kirari/src/languages/dart.ts b/packages/kirari/src/languages/dart.ts index 6679d7b..d51920e 100644 --- a/packages/kirari/src/languages/dart.ts +++ b/packages/kirari/src/languages/dart.ts @@ -4,7 +4,7 @@ import { nestedComments, whitespace, lineComment, blockComment, quotedString, tr export const dart: Language = { name: 'dart', - definitions: definitionsFor('dart'), + definitions: definitionsFor({ arrow: '=>', typeParameters: true, parameterBodies: true, genericCalls: true }), tokenize: nestedComments, identifier: /[$_\p{ID_Start}][$\p{ID_Continue}]*/u, keywords: 'abstract as assert async await base break case catch class const continue covariant default deferred do dynamic else enum export extends extension external factory final finally for get hide if implements import in interface is late library mixin native new of on operator part required rethrow return sealed set show static super switch sync this throw try typedef var void when while with yield'.split(' '), diff --git a/packages/kirari/src/languages/elixir.ts b/packages/kirari/src/languages/elixir.ts index e2ec4fe..7124258 100644 --- a/packages/kirari/src/languages/elixir.ts +++ b/packages/kirari/src/languages/elixir.ts @@ -1,4 +1,5 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' import { whitespace } from '../grammars/clike.js' @@ -7,9 +8,18 @@ const heredoc = /"""(?:\\(?:[\s\S]|$)|(?!""")[^\\])*(?:"""|$)|'''(?:\\(?:[\s\S]| const delimited = /\((?:\\(?:[\s\S]|$)|[^)\\])*(?:\)|$)|\[(?:\\(?:[\s\S]|$)|[^\]\\])*(?:\]|$)|\{(?:\\(?:[\s\S]|$)|[^}\\])*(?:\}|$)|<(?:\\(?:[\s\S]|$)|[^>\\])*(?:>|$)|([/|])(?:\\(?:[\s\S]|$)|(?!\1)[^\\])*(?:\1|$)/ const sigil = new RegExp(`(?:${heredoc.source}|${quoted.source}|${delimited.source})[a-zA-Z0-9]*`) +// Captures: `&(&1 + 1)` and `&Module.fun/1`. +const capture: FunctionSyntax['forms'] = first => first === '&' ? true : undefined + +const functions: FunctionSyntax = { + declaration: /^(defp?|defmacro[p]?|defguard[p]?|defdelegate)$/, + literal: /^fn$/, + forms: capture, +} + export const elixir: Language = { name: 'elixir', - definitions: definitionsFor('elixir'), + definitions: definitionsFor(functions), identifier: /[_\p{ID_Start}]\p{ID_Continue}*[!?]?/u, keywords: 'after alias and case catch cond def defdelegate defexception defguard defguardp defimpl defmacro defmacrop defmodule defp defprotocol defstruct do else end fn for if import in not or quote raise receive require rescue reraise super throw try unless unquote unquote_splicing use when with'.split(' '), booleans: ['true', 'false'], diff --git a/packages/kirari/src/languages/glsl.ts b/packages/kirari/src/languages/glsl.ts index e74ed46..8353114 100644 --- a/packages/kirari/src/languages/glsl.ts +++ b/packages/kirari/src/languages/glsl.ts @@ -4,7 +4,7 @@ import { identifier, whitespace, lineComment, blockComment, number, operator, pu export const glsl: Language = { name: 'glsl', - definitions: definitionsFor('glsl'), + definitions: definitionsFor({}), identifier, keywords: 'attribute break buffer case centroid coherent const continue default discard do else flat for highp if in inout invariant layout lowp mediump noperspective out patch precision readonly restrict return sample shared smooth struct subroutine switch uniform varying volatile while writeonly'.split(' '), types: 'void bool int uint float double atomic_uint'.split(' '), diff --git a/packages/kirari/src/languages/go.ts b/packages/kirari/src/languages/go.ts index 4e6861a..f3a8fe7 100644 --- a/packages/kirari/src/languages/go.ts +++ b/packages/kirari/src/languages/go.ts @@ -1,10 +1,20 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' import { identifier, whitespace, lineComment, blockComment, quotedString, number, operator, punctuation } from '../grammars/clike.js' +const functions: FunctionSyntax = { + declaration: /^func$/, + literal: /^func$/, + literalBody: true, + typeBodies: /^(struct|interface)$/, + varAnnotations: true, + assignment: /^(=|:=)$/, +} + export const go: Language = { name: 'go', - definitions: definitionsFor('go'), + definitions: definitionsFor(functions), identifier, keywords: 'break case chan const continue default defer else fallthrough for func go goto if import interface map package range return select struct switch type var'.split(' '), types: 'any bool byte comparable complex64 complex128 error float32 float64 int int8 int16 int32 int64 rune string uint uint8 uint16 uint32 uint64 uintptr'.split(' '), diff --git a/packages/kirari/src/languages/groovy.ts b/packages/kirari/src/languages/groovy.ts index 650b2f5..7735267 100644 --- a/packages/kirari/src/languages/groovy.ts +++ b/packages/kirari/src/languages/groovy.ts @@ -1,4 +1,4 @@ -import { definitionsFor } from '../grammars/definitions.js' +import { braceClosure, definitionsFor } from '../grammars/definitions.js' import { scanWithOverrides } from '../tokenize.js' import type { Language, Token } from '../types.js' import { whitespace, lineComment, blockComment, quotedString, tripleString, number, operator, punctuation } from '../grammars/clike.js' @@ -18,7 +18,7 @@ const slashyStrings = (source: string, language: Language): Token[] => { export const groovy: Language = { name: 'groovy', - definitions: definitionsFor('groovy'), + definitions: definitionsFor({ forms: braceClosure }), tokenize: slashyStrings, identifier: /[$_\p{ID_Start}][$\p{ID_Continue}]*/u, keywords: 'abstract as assert break case catch class const continue def default do else enum extends final finally for goto if implements import in instanceof interface native new non-sealed package permits private protected public record return sealed static strictfp super switch synchronized this throw throws trait transient try var void volatile while yield'.split(' '), diff --git a/packages/kirari/src/languages/java.ts b/packages/kirari/src/languages/java.ts index dea36d3..f897e4b 100644 --- a/packages/kirari/src/languages/java.ts +++ b/packages/kirari/src/languages/java.ts @@ -4,7 +4,7 @@ import { whitespace, lineComment, blockComment, tripleString, quotedString, numb export const java: Language = { name: 'java', - definitions: definitionsFor('java'), + definitions: definitionsFor({ arrow: '->', genericCalls: true }), identifier: /[$_\p{ID_Start}][$\p{ID_Continue}]*/u, keywords: 'abstract assert break case catch class const continue default do else enum exports extends final finally for goto if implements import instanceof interface module native new non-sealed open opens package permits private protected provides public record requires return sealed static strictfp super switch synchronized this throw throws to transient transitive try uses var volatile when while with yield'.split(' '), types: 'boolean byte char double float int long short void'.split(' '), diff --git a/packages/kirari/src/languages/julia.ts b/packages/kirari/src/languages/julia.ts index 67869a4..690ab2a 100644 --- a/packages/kirari/src/languages/julia.ts +++ b/packages/kirari/src/languages/julia.ts @@ -19,7 +19,7 @@ const comments = (source: string, language: Language): Token[] => { export const julia: Language = { name: 'julia', - definitions: definitionsFor('julia'), + definitions: definitionsFor({ declaration: /^(function|macro)$/, literal: /^function$/, arrow: '->' }), tokenize: comments, identifier: /[_\p{ID_Start}][\p{ID_Continue}]*(?:!(?!=))?/u, keywords: 'abstract baremodule begin break catch const continue do else elseif end export finally for function global if import in isa let local macro module mutable outer primitive public quote return struct try type using where while'.split(' '), diff --git a/packages/kirari/src/languages/kotlin.ts b/packages/kirari/src/languages/kotlin.ts index 0494704..78895fd 100644 --- a/packages/kirari/src/languages/kotlin.ts +++ b/packages/kirari/src/languages/kotlin.ts @@ -1,10 +1,10 @@ -import { definitionsFor } from '../grammars/definitions.js' +import { braceClosure, definitionsFor } from '../grammars/definitions.js' import type { Language } from '../types.js' import { nestedComments, identifier, whitespace, lineComment, blockComment, quotedString, number, operator, punctuation } from '../grammars/clike.js' export const kotlin: Language = { name: 'kotlin', - definitions: definitionsFor('kotlin'), + definitions: definitionsFor({ declaration: /^fun$/, literal: /^fun$/, literalBody: true, forms: braceClosure, annotations: true }), tokenize: nestedComments, identifier, keywords: 'actual abstract annotation as break by catch class companion const constructor context continue crossinline data delegate do dynamic else enum expect external field file final finally for fun get if import in infix init inline inner interface internal is lateinit noinline object open operator out override package param private property protected public receiver reified return sealed set setparam super suspend tailrec this throw try typealias typeof val value var vararg when where while'.split(' '), diff --git a/packages/kirari/src/languages/lua.ts b/packages/kirari/src/languages/lua.ts index 4b3a9e3..03fc710 100644 --- a/packages/kirari/src/languages/lua.ts +++ b/packages/kirari/src/languages/lua.ts @@ -20,7 +20,7 @@ const longBrackets = (source: string, language: Language): Token[] => { export const lua: Language = { name: 'lua', - definitions: definitionsFor('lua'), + definitions: definitionsFor({ declaration: /^function$/, literal: /^function$/ }), tokenize: longBrackets, identifier: /[a-zA-Z_]\w*/, keywords: 'and break do else elseif end for function goto if in local not or repeat return then until while'.split(' '), diff --git a/packages/kirari/src/languages/php.ts b/packages/kirari/src/languages/php.ts index b7bb725..99cf8d1 100644 --- a/packages/kirari/src/languages/php.ts +++ b/packages/kirari/src/languages/php.ts @@ -6,7 +6,7 @@ const keywords = 'abstract and array as break callable case catch class clone co export const php: Language = { name: 'php', - definitions: definitionsFor('php'), + definitions: definitionsFor({ declaration: /^function$/i, literal: /^(function|fn)$/i, literalBody: true, modifiers: /^static$/i }), identifier: /[_\p{ID_Start}]\p{ID_Continue}*/u, keywords, types: 'bool float int iterable mixed never object string void'.split(' '), diff --git a/packages/kirari/src/languages/powershell.ts b/packages/kirari/src/languages/powershell.ts index a06a8e4..ba5c8a9 100644 --- a/packages/kirari/src/languages/powershell.ts +++ b/packages/kirari/src/languages/powershell.ts @@ -1,4 +1,4 @@ -import { definitionsFor } from '../grammars/definitions.js' +import { braceClosure, definitionsFor } from '../grammars/definitions.js' import type { Language } from '../types.js' import { whitespace } from '../grammars/clike.js' @@ -7,7 +7,7 @@ const boundary = /(?![\p{L}\p{Nd}_?-])/u export const powershell: Language = { name: 'powershell', - definitions: definitionsFor('powershell'), + definitions: definitionsFor({ declaration: /^(function|filter|workflow)$/i, forms: braceClosure }), rules: [ whitespace, { type: 'comment', pattern: /#[^\r\n]*|<#[\s\S]*?(?:#>|$)/ }, diff --git a/packages/kirari/src/languages/protobuf.ts b/packages/kirari/src/languages/protobuf.ts index 07695b4..3b705bb 100644 --- a/packages/kirari/src/languages/protobuf.ts +++ b/packages/kirari/src/languages/protobuf.ts @@ -4,7 +4,7 @@ import type { Language } from '../types.js' export const protobuf: Language = { name: 'protobuf', - definitions: definitionsFor('protobuf'), + definitions: definitionsFor({ declaration: /^rpc$/ }), identifier: /[A-Za-z_]\w*/, keywords: ['syntax', 'edition', 'import', 'weak', 'public', 'package', 'option', 'message', 'enum', 'service', 'rpc', 'returns', 'stream', 'oneof', 'map', 'reserved', 'to', 'max', 'extensions', 'extend', 'required', 'optional', 'repeated', 'group'], types: ['double', 'float', 'int32', 'int64', 'uint32', 'uint64', 'sint32', 'sint64', 'fixed32', 'fixed64', 'sfixed32', 'sfixed64', 'bool', 'string', 'bytes'], diff --git a/packages/kirari/src/languages/python.ts b/packages/kirari/src/languages/python.ts index 2e49b42..a275b31 100644 --- a/packages/kirari/src/languages/python.ts +++ b/packages/kirari/src/languages/python.ts @@ -1,4 +1,5 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import { scan } from '../tokenize.js' import { isSignificant } from '../types.js' import type { Language, Token } from '../types.js' @@ -146,9 +147,18 @@ const scanPython: ResumableScan = (source, language, from, checkpoint) => { const tokenizePython = (source: string, language: Language): Token[] => scanPython(source, language) +const functions: FunctionSyntax = { + declaration: /^def$/, + literal: /^lambda$/, + comparisonAngles: true, + annotations: true, + lineAnnotations: true, + decorators: true, +} + export const python: Language = { name: 'python', - definitions: definitionsFor('python'), + definitions: definitionsFor(functions), tokenize: tokenizePython, identifier: /[_\p{ID_Start}]\p{ID_Continue}*/u, keywords: 'and as assert async await break case class continue def del elif else except finally for from global if import in is lambda match nonlocal not or pass raise return try while with yield'.split(' '), diff --git a/packages/kirari/src/languages/r.ts b/packages/kirari/src/languages/r.ts index bbdc5a7..6c5d73e 100644 --- a/packages/kirari/src/languages/r.ts +++ b/packages/kirari/src/languages/r.ts @@ -21,7 +21,7 @@ const rawStrings = (source: string, language: Language): Token[] => { export const r: Language = { name: 'r', - definitions: definitionsFor('r'), + definitions: definitionsFor({ literal: /^(function|\\)$/, assignment: /^(=|<-|<<-)$/ }), tokenize: rawStrings, identifier: /(?:[\p{L}]|\.(?!\d))[\p{L}\d._]*/u, keywords: 'if else repeat while function for in next break'.split(' '), diff --git a/packages/kirari/src/languages/ruby.ts b/packages/kirari/src/languages/ruby.ts index ba709d4..220e3a3 100644 --- a/packages/kirari/src/languages/ruby.ts +++ b/packages/kirari/src/languages/ruby.ts @@ -1,12 +1,29 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' import { whitespace, number, operator, punctuation } from '../grammars/clike.js' const percentBody = /(?:\{(?:\\(?:[\s\S]|$)|[^}\\])*(?:\}|$)|\((?:\\(?:[\s\S]|$)|[^)\\])*(?:\)|$)|\[(?:\\(?:[\s\S]|$)|[^\]\\])*(?:\]|$)|<(?:\\(?:[\s\S]|$)|[^>\\])*(?:>|$)|([^\w\s{([<])(?:\\(?:[\s\S]|$)|(?!\1)[^\\])*(?:\1|$))/ +// Stabby lambdas: `->(x) { … }` and `-> do … end`. +const stabby: FunctionSyntax['forms'] = (first, start, limit, { value, closing, block }) => { + if (first !== '->') return undefined + let body = start + 1 + if (value(body) === '(') body = (closing(body) ?? limit) + 1 + return value(body) === 'do' || value(body) === '{' && block(body) +} + +const functions: FunctionSyntax = { + declaration: /^def$/, + literal: /^(lambda|proc)$/, + literalOpener: /^(\{|do|\()$/, + literalBody: true, + forms: stabby, +} + export const ruby: Language = { name: 'ruby', - definitions: definitionsFor('ruby'), + definitions: definitionsFor(functions), regexLiterals: true, identifier: /[_\p{ID_Start}]\p{ID_Continue}*[!?]?/u, keywords: 'BEGIN END alias and begin break case class def defined? do else elsif end ensure for if in module next not or redo rescue retry return self super then undef unless until when while yield __ENCODING__ __FILE__ __LINE__'.split(' '), diff --git a/packages/kirari/src/languages/rust.ts b/packages/kirari/src/languages/rust.ts index 69c8a1d..df1e014 100644 --- a/packages/kirari/src/languages/rust.ts +++ b/packages/kirari/src/languages/rust.ts @@ -1,10 +1,14 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' import { nestedComments, identifier, whitespace, lineComment, blockComment, number, operator, punctuation } from '../grammars/clike.js' +// Closures: `|x| x + 1` and `|| …`. +const closure: FunctionSyntax['forms'] = first => first === '|' || first === '||' ? true : undefined + export const rust: Language = { name: 'rust', - definitions: definitionsFor('rust'), + definitions: definitionsFor({ declaration: /^fn$/, modifiers: /^(async|move)$/, forms: closure, genericCalls: true, annotations: true }), tokenize: nestedComments, identifier, keywords: 'as async await break const continue crate dyn else enum extern fn for if impl in let loop match mod move mut pub ref return self Self static struct super trait type union unsafe use where while abstract become box do final macro override priv typeof unsized virtual yield try gen'.split(' '), diff --git a/packages/kirari/src/languages/scala.ts b/packages/kirari/src/languages/scala.ts index bf5dc5b..878fcc1 100644 --- a/packages/kirari/src/languages/scala.ts +++ b/packages/kirari/src/languages/scala.ts @@ -1,10 +1,26 @@ import { definitionsFor } from '../grammars/definitions.js' +import type { FunctionSyntax } from '../grammars/definitions.js' import type { Language } from '../types.js' import { nestedComments, whitespace, lineComment, blockComment, quotedString, number, operator, punctuation } from '../grammars/clike.js' +// Function literals in braces, `{ case … => … }` and `{ x => … }`, and polymorphic `[T] => …`. +const lambda: FunctionSyntax['forms'] = (first, start, limit, { value, closing, unclosed, block }) => { + if (first === '[') { + const end = closing(start) + return end !== undefined && value(end + 1) === '=>' + } + if (first !== '{') return undefined + for (let cursor = start + 1; cursor < (closing(start) ?? limit); cursor++) { + if (value(cursor) === '=>') return block(start) + if (unclosed(cursor)) return false + cursor = closing(cursor) ?? cursor + } + return false +} + export const scala: Language = { name: 'scala', - definitions: definitionsFor('scala'), + definitions: definitionsFor({ declaration: /^def$/, arrow: '=>', forms: lambda, annotations: true }), tokenize: nestedComments, identifier: /[$_\p{ID_Start}][$\p{ID_Continue}]*/u, keywords: 'abstract as case catch class def derives do else end enum export extends extension final finally for forSome given if implicit import infix inline lazy match new object opaque open override package private protected return sealed super then this throw trait transparent try type using val var while with yield'.split(' '), diff --git a/packages/kirari/src/languages/shell.ts b/packages/kirari/src/languages/shell.ts index 308ba0e..a4bac26 100644 --- a/packages/kirari/src/languages/shell.ts +++ b/packages/kirari/src/languages/shell.ts @@ -161,7 +161,7 @@ const heredocEnd = (source: string, position: number, heredoc: Heredoc): { start export const shell: Language = { name: 'shell', - definitions: definitionsFor('shell'), + definitions: definitionsFor({ declaration: /^function$/ }), tokenize: tokenizeShell, identifier: /[a-zA-Z_][a-zA-Z_\d-]*/, keywords: 'case do done elif else esac fi for function if in select then time until while'.split(' '), diff --git a/packages/kirari/src/languages/sql.ts b/packages/kirari/src/languages/sql.ts index 15626f6..c45b101 100644 --- a/packages/kirari/src/languages/sql.ts +++ b/packages/kirari/src/languages/sql.ts @@ -3,7 +3,7 @@ import type { Language } from '../types.js' export const sql: Language = { name: 'sql', - definitions: definitionsFor('sql'), + definitions: definitionsFor({ declaration: /^(function|procedure)$/i }), rules: [ { type: 'text', pattern: /\s+/ }, { type: 'comment', pattern: /--[^\r\n]*|\/\*[^]*?(?:\*\/|$)/ }, diff --git a/packages/kirari/src/languages/swift.ts b/packages/kirari/src/languages/swift.ts index 25fedef..7a2b042 100644 --- a/packages/kirari/src/languages/swift.ts +++ b/packages/kirari/src/languages/swift.ts @@ -1,10 +1,10 @@ -import { definitionsFor } from '../grammars/definitions.js' +import { braceClosure, definitionsFor } from '../grammars/definitions.js' import type { Language } from '../types.js' import { nestedComments, identifier, whitespace, lineComment, blockComment, tripleString, doubleString, number, operator, punctuation } from '../grammars/clike.js' export const swift: Language = { name: 'swift', - definitions: definitionsFor('swift'), + definitions: definitionsFor({ declaration: /^func$/, forms: braceClosure, genericCalls: true, annotations: true }), tokenize: nestedComments, identifier, keywords: 'actor any as associatedtype async await borrowing break case catch class consuming continue convenience copy default defer deinit didSet distributed do dynamic each else enum extension fallthrough fileprivate final for func get guard if import in indirect infix init inout internal is isolated lazy let macro mutating nonisolated nonmutating open operator optional override package postfix precedencegroup prefix private protocol public repeat required rethrows return self Self sending set some static struct subscript super switch throws throw try typealias unowned var weak where while willSet'.split(' '), diff --git a/packages/kirari/src/languages/zig.ts b/packages/kirari/src/languages/zig.ts index 54e2e55..f164ef9 100644 --- a/packages/kirari/src/languages/zig.ts +++ b/packages/kirari/src/languages/zig.ts @@ -4,7 +4,7 @@ import { identifier, whitespace, lineComment, quotedString, number, operator, pu export const zig: Language = { name: 'zig', - definitions: definitionsFor('zig'), + definitions: definitionsFor({ declaration: /^fn$/ }), identifier, keywords: 'addrspace align allowzero and anyframe anytype asm async await break callconv catch comptime const continue defer else enum errdefer error export extern fn for if inline linksection noalias noinline nosuspend opaque or orelse packed pub resume return section struct suspend switch test threadlocal try union unreachable usingnamespace var volatile while'.split(' '), types: 'anyerror bool c_char c_short c_ushort c_int c_uint c_long c_ulong c_longlong c_ulonglong c_longdouble comptime_float comptime_int f16 f32 f64 f80 f128 isize usize noreturn type void anyopaque'.split(' '), diff --git a/packages/kirari/src/stream/resumable.ts b/packages/kirari/src/stream/resumable.ts index 1c9a4a5..1f09928 100644 --- a/packages/kirari/src/stream/resumable.ts +++ b/packages/kirari/src/stream/resumable.ts @@ -2,7 +2,7 @@ import type { TokenPatch, TokenStream } from '../stream.js' import type { Language, Token } from '../types.js' import { isSignificant } from '../types.js' import type { ResumableScan, ResumePoint } from '../resumable.js' -import { classifyDefinitions, endsLine } from '../grammars/definitions.js' +import { endsLine } from '../grammars/definitions.js' import { advanceDefinitionState, breakDefinitionLine, copyDefinitionState, createDefinitionState, isDefinitionBoundary } from '../grammars/definitions/state.js' import type { DefinitionState } from '../grammars/definitions/state.js' @@ -56,7 +56,7 @@ export const createResumableStream = (language: Language, scanResumable: Resumab if (last) { resume = last.resume; resumeIndex = last.index } const from = final.offset - const tokens = classifyDefinitions(raw.slice(final.index), language.name, final.open) + const tokens = language.definitions!(raw.slice(final.index), final.open) // Tokens before the last resume point are final. A point is decided once the first significant // token after it is among them; its answer cannot change after that. let decided = 0 From 4a72eb1fa8b9a936b29c8b2d9533cc4e92a73dbb Mon Sep 17 00:00:00 2001 From: ugi-dev Date: Thu, 22 Oct 2026 16:05:00 +1100 Subject: [PATCH 2/2] docs: add architecture guide and refresh bundle sizes ARCHITECTURE.md maps the core package for contributors, including per-language FunctionSyntax. Documented sizes now match bun run size: core 3,735, JavaScript 4,252, all languages 23,171, highlight() wrapper 4,153 bytes gzip. --- ARCHITECTURE.md | 132 ++++++++++++++++++++++++++++++ CONTRIBUTING.md | 2 + docs/comparison.md | 6 +- packages/kirari/README.md | 6 +- sites/docs/content/limitations.md | 2 +- sites/docs/src/lib/site.ts | 2 +- 6 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 ARCHITECTURE.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..95a7218 --- /dev/null +++ b/ARCHITECTURE.md @@ -0,0 +1,132 @@ +# Architecture + +This is a map of the core package, `packages/kirari/src`, for people changing it. The user +documentation lives in [docs](docs) and on [kirari.sh](https://kirari.sh/docs/). + +Kirari turns source text into a flat list of tokens, then renders them. Every token is an +unchanged slice of the source with UTF-16 offsets, and the tokens always cover the whole source +in order. Nothing else is shared between stages: if you can produce such a list, every renderer, +decoration, and integration works with it. + +``` +source ──▶ language.tokenize or scan ──▶ language.definitions ──▶ tokens ──▶ render + (lexing, with context) (function names) renderDecorated + react, rehype +``` + +## Languages are data + +A language (`types.ts`, `Language`) is mostly data: ordered regex `rules`, an `identifier` +pattern, and word lists for keywords, types, booleans, and nulls. Most files in `languages/` +are only that. Shared rules for C-like syntax are in `grammars/clike.ts`. + +A language may add two hooks: + +- `tokenize` replaces the default scanner. Use it when rules cannot express a construct, such as + heredocs (`shell.ts`), nested comments (`clike.ts`, `nestedComments`), or Markdown blocks. + Most hooks still call `scan` and adjust its output. `scanWithOverrides` covers the common case + of extending a token, such as a Lua long bracket, and restarting the scan after it. +- `definitions` reclassifies names after lexing. Built-in languages use + `definitionsFor(syntax)`; see [Function names](#function-names). + +## The scanner + +`tokenize.ts` holds `scan`, the scanner every rule-based language uses. At each offset it tries, +in order: + +1. template text, when inside a JavaScript template literal +2. a regex literal, when the language has them and the previous token allows one +3. the language rules; the first match wins +4. a template opening +5. an identifier, which is then classified as a keyword, type, property, function, or variable +6. a single character of text + +Earlier tokens decide how later ones read, so the scanner carries state: + +| State | What it decides | Example | +| --- | --- | --- | +| `previous` | the last significant token (not text or comment) | `/` after `=` is a regex, after `x` it is division | +| `frames` | template literal nesting | where `${…}` ends | +| `ScanContext` | braces, parentheses, and TypeScript type positions | `void` in `(): void` is a type | + +`scan` is a generator. A caller can pass an offset to `next()` to continue somewhere else. Markup +uses this to hand control back after an embedded JSX element. + +## Function look-ahead (JavaScript and TypeScript) + +For a name in a JavaScript-like language, the scanner may look ahead to decide whether it is a +function: `name(`, `name(`, `name = async (x) => …`, or `name: function () {}`. The +look-ahead (`grammars/functions.ts`) runs a second scanner from the end of the name and steps over +whole bracket groups. It caches group ends per source, so long signatures are scanned once. + +Its result includes `pending` when the answer depends on source that has not arrived yet. Streams +use that to avoid treating such a token as final. + +## Function names in other languages + +Other languages decide function names after lexing, in `grammars/definitions.ts`. +`classifyDefinitions` works on significant tokens only. It first matches brackets, then marks a +name as a function when it is: + +- declared: `def name`, `fn name`, including qualified names such as `def self.name` +- called: `name(`, or `name(` where the language has generic calls +- bound to a function literal: `name = lambda …`, `name: T = |x| …`, `(def name (fn …))` + +The algorithm contains no language names. Each language passes a `FunctionSyntax` describing +how it writes functions: its declaration and literal keywords, lambda arrow, annotation style, +and hooks (`forms`, `binding`) for syntax no flag covers, such as C++ lambdas or Clojure `let` +vectors. To support a new language, describe its syntax in its own file; change +`definitions.ts` only when the algorithm itself needs a new capability. + +## Markup + +`grammars/markup.ts` scans HTML, XML, Vue and Svelte templates, and JSX/TSX with a stack of +frames. A markup frame tracks a tag and its attributes. A code frame wraps a script scanner over +the same source offsets, so expressions inside `{…}` and `