5.3 KiB
| author | date | issue |
|---|---|---|
| Steven R. Loomis <srl295@gmail.com> | 2025-05-19 | https://github.com/keymanapp/keyman/issues/14020 |
Keyman Developer Messaging
Summary
This document discusses the mechanism for kmc Compiler + Toolchain Messages in Keyman Developer.
Objectives
- Provide developers (end users) with feedback on the failure (or success) of their use of Keyman Developer tools. (Compilers and others: All compilers are tools, but not all tools are compilers.)
- Enable that feedback to be actionable:
- Clear: so the developer knows what happened.
- Concise: so the developer can identify the relevant parts of the message without extraneous information. Enable deduplication: allow the consumer of the messages to deduplicate messages, but don't deduplicate them prematurely.
- Context: so the developer knows where the issue happened. Exactly what part of the process, which file, which line/column number. Enable presentation of failing LOC by message consumer
- Empower the #developer-team of Keyman to effectively author these messages, that is, encourage deployment of messages meeting above goals
- Contain ceremony (repetitive content) around message call sites
- declutter the calling code
- encourage developers to make use of distinct messaging vs. 'catch-alls': "Something Bad Happened [Here]"
- Contain burden on tool/platform implementation code
- Single mechanism for reporting (and it's not
console.log).- we don't expect exceptions to bubble up to users, that's an exceptional [sic] case.
- publish messages to the website
- validation/ linting
- formal representation of parameterized messages rather than string concatenation or in-compiler message massaging code
- in short, string content should live in the messages, not the compiler
- Contain ceremony (repetitive content) around message call sites
- Responsibility of consumer (i.e. CLI tool, IDE) to:
- format
- display
- future: localize ( i18n )?
Details of the Current Mechanism
Types
CompilerEvent
filename— stringoffset— number, in some cases, resolves to line:columnline— number, may be set directolycolumn— number, may be set directlycode— hex, see belowmessage— resolved formatted textdetail— Markdown with more detail and suggestionsexceptionVar— used for internal tracking of exceptions
Code
- https://help.keyman.com/developer/current-version/reference/messages/
- Example:
KM03009: ERROR_UnknownWordBreaker
00503009- hex valueRRSNNBBB- (see below)RR______- Reserved__S_____- Severity, example,5= error___NN___- Namespace, example03= model compiler_____BBB- Base error, ex009= "unknown word breaker"
Callback
The callbacks object includes the function:
-
reportMessage(e: CompilerEvent)provided by the consumerWe have chained/wrapper callbacks in some cases.
Metadata
- The fast-xml-parser package has an option to generate metadata (via a special symbol property) on each object in the parse tree.
- This metadata includes line numbering.
interface XMLMetaData { startIndex?: number };
- We extend this metadata concept to include symbols for import status and filename, in the case of imported files.
Flow
Call Site
Example: keys.ts - compiler - call site for error
this.callbacks.reportMessage(
LdmlCompilerMessages.Error_InvalidScanCode({
invalidCodeList: codes.join(' '), id:x?.id }, form)
);
{ id, codes }- message parameters (optional)form- object that caused the error (contains line number metadata)
Message Expansion
Message expansion m() or mx() (in LDML) function is used.
static ERROR_InvalidScanCode = SevError | 0x0009;
static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, compileContext:ObjectWithCompileContext) => mx(
this.ERROR_InvalidScanCode, context,
Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}',
`…additional markdown detail…`
);
Parameters:
- 'o' (Object) used for parameters- here, the object id and the codes.
- 'x' (conteXt) used for extracting the offset, which will be resolved to line numbers.
Q: But wait, doesn't x.id already have the 'id'? DRY!?
A: Yes, however, we want a clean separation between parameters and context. This way, even though repetitive, makes it clear exactly what's being passed and consumed as a parameter. x has other irrelevent stuff on it.
That is, no additional code in the *_*() function (the caller of m(), mx() - put helper functions outside ,move into the call site)
Context extraction
The mx() function does the following (note that m() is called first).
let evt = m(code, message, detail); // raw message
evt = LdmlCompilerMessages.offset(evt, compileContext); // with offset
Note: We could have: ml(e:CompilerEvent, lineNumber?: number) so that compilers with a line number could pass that in directly.
Callbacks
reportMessage()
- pre-process if needed - expand
offset(ex: byte offset 276) toline:column(line 13 column 4) - "end user" of compiler receives result with
messages[]array