fix(developer): emit compile error when unsupported statements encountered in stores

When a store contains any unsupported statement (any, baselayout, beep,
call, context, if, index, layer, notany, nul, platform, reset, save,
set, use) or undocumented statement (clearcontext, fix, switch), then
emit a compile error. Previously, compiler would not detect use of these
statements in stores, which could lead to undefined behaviour at
runtime.

At time of writing, eight keyboards in the keyboards repository
(clavbur9, hausa_ajami_qwerty, sil_cameroon_qwerty, sil_cameroon_azerty,
sil_dzongkha, sil_yoruba8, sil_yoruba_bar, sil_yoruba_dot) make use of
either beep or nul in stores. Due to compiler unfolding of any/index
statements for key part of the rule, these would work in some cases, but
this was never supported explicitly nor completely, and so at this point
it is better to prevent misuse and add full support as a future task.

Fixes: #16718
Test-bot: skip
This commit is contained in:
Marc Durdin 2026-10-03 11:41:42 +02:00
parent 6ce0b0e1ac
commit ef212c7e66
27 changed files with 277 additions and 3 deletions

View file

@ -289,6 +289,8 @@ namespace KmnCompilerMessages {
WARN_DeprecatedValueFormat = SevWarn | 0x0BC,
WARN_DeprecatedCompileTarget = SevWarn | 0x0BD,
ERROR_StoreContainsUnsupportedStatement = SevError | 0x0BE,
FATAL_BufferOverflow = SevFatal | 0x0C0
// FATAL_Break = SevFatal | 0x0C1, unused
};

View file

@ -930,6 +930,12 @@ export class KmnCompilerMessages {
`Compile target '${def(o.p?.[0])}' was deprecated in Keyman 19 and will cause the line to always be ignored by kmc in Keyman 20.`,
);
static ERROR_StoreContainsUnsupportedStatement = SevError | 0x0BE;
static Error_StoreContainsUnsupportedStatement = () => mc(
this.ERROR_StoreContainsUnsupportedStatement,
`A store can contain only characters, deadkeys, virtual keys, and 'outs()' statements.`,
);
static FATAL_BufferOverflow = SevFatal | 0x0C0;
static Fatal_BufferOverflow = () => mc(this.FATAL_BufferOverflow, `The compiler memory buffer overflowed`);

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-any'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(letter) 'F'
c ERROR_StoreContainsUnsupportedStatement
store(bad-any) any(letter)

View file

@ -0,0 +1,12 @@
store(&NAME) 'error_store_contains_unsupported_statement-baselayout'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement; note baselayout() compiles to CODE_IFSYSTEMSTORE
store(bad-baselayout) baselayout('us')

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-beep'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-beep) beep

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-call'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(callee) 'dll.dll:func'
c ERROR_StoreContainsUnsupportedStatement
store(bad-call) call(callee)

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-clearcontext'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement; note clearcontext undocumented, deprecated in v19
store(bad-clearcontext) clearcontext

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-context'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-context) context

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-contextex'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-contextex) context(1)

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-fix'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement; note fix undocumented, deprecated in v19
store(bad-fix) fix

View file

@ -0,0 +1,12 @@
store(&NAME) 'error_store_contains_unsupported_statement-ifopt'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement
store(bad-if) if(opt='F')

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-ifsystemstore'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-ifs) if(&layer='ah') 'eek'

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-index'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(letter) 'F'
c ERROR_StoreContainsUnsupportedStatement
store(bad-index) index(letter, 1)

View file

@ -0,0 +1,12 @@
store(&NAME) 'error_store_contains_unsupported_statement-layer'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement; note layer() compiles to CODE_IFSYSTEMSTORE
store(bad-layer) layer('shift')

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-notany'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(letter) 'F'
c ERROR_StoreContainsUnsupportedStatement
store(bad-notany) notany(letter)

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-nul'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-nul) nul

View file

@ -0,0 +1,12 @@
store(&NAME) 'error_store_contains_unsupported_statement-platform'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement; note platform() compiles to CODE_IFSYSTEMSTORE
store(bad-platform) platform('windows')

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-resetopt'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement
store(bad-reset) reset(opt)

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-return'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-reset) return

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-saveopt'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement
store(bad-save) save(opt)

View file

@ -0,0 +1,11 @@
store(&NAME) 'error_store_contains_unsupported_statement-setopt'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
store(opt) 'opt'
c ERROR_StoreContainsUnsupportedStatement
store(bad-set) set(opt='X')

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-setsystemstore'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-setsystemstore) set(&layer='X')

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-switch'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement; note switch undocumented and unused, should be eliminated
store(bad-setsystemstore) switch(1)

View file

@ -0,0 +1,9 @@
store(&NAME) 'error_store_contains_unsupported_statement-use'
store(&VERSION) '9.0'
begin Unicode > use(main)
group(main) using keys
c ERROR_StoreContainsUnsupportedStatement
store(bad-use) use(main)

View file

@ -412,8 +412,19 @@ describe('KmnCompilerMessages', function () {
// WARN_LanguageHeadersDeprecatedInKeyman10
['ethnologuecode', 'language', 'windowslanguages'].forEach(storeName =>
it(`should generate WARN_LanguageHeadersDeprecatedInKeyman10 if the contains deprecated system store "&${storeName}`, async function() {
it(`should generate WARN_LanguageHeadersDeprecatedInKeyman10 if the file contains deprecated system store "&${storeName}"`, async function() {
await testForMessage(this, ['keyboards', `warn_language_headers_deprecated_in_keyman10-${storeName}.kmn`], KmnCompilerMessages.WARN_LanguageHeadersDeprecatedInKeyman10);
})
);
// ERROR_StoreContainsUnsupportedStatement
['any', 'index', 'clearcontext', 'fix', 'context', 'nul', 'use', 'return', 'beep', 'switch',
'call', 'contextex', 'notany', 'setopt',
'ifopt', 'baselayout', 'layer', 'platform',
'saveopt', 'resetopt', 'ifsystemstore', 'setsystemstore'].forEach(statement =>
it(`should generate ERROR_StoreContainsUnsupportedStatement if a store contains statement "${statement}"`, async function() {
await testForMessage(this, ['invalid-keyboards', `error_store_contains_unsupported_statement-${statement}.kmn`], KmnCompilerMessages.ERROR_StoreContainsUnsupportedStatement);
})
);
});

View file

@ -139,6 +139,7 @@ KMX_DWORD GetRHS(PFILE_KEYBOARD fk, PKMX_WCHAR p, PKMX_WCHAR buf, int bufsize, i
PKMX_WCHAR GetDelimitedString(PKMX_WCHAR *p, KMX_WCHAR const * Delimiters, KMX_WORD Flags);
KMX_DWORD GetXString(PFILE_KEYBOARD fk, PKMX_WCHAR str, KMX_WCHAR const * token, PKMX_WCHAR output, int max, int offset, PKMX_WCHAR *newp, int isVKey, int isUnicode);
KMX_BOOL GetCompileTargetsFromTargetsStore(KMX_WCHAR *store, int &targets);
KMX_BOOL DoesStoreContainInvalidStatement(PKMX_WCHAR store);
int GetGroupNum(PFILE_KEYBOARD fk, PKMX_WCHAR p);
@ -843,6 +844,11 @@ KMX_BOOL ProcessStoreLine(PFILE_KEYBOARD fk, PKMX_WCHAR p) {
kmcmp::CodeConstants->reindex(); // has to be done after every character add due to possible use in another store. // I4982
}
if(DoesStoreContainInvalidStatement(sp->dpString)) {
ReportCompilerMessage(KmnCompilerMessages::ERROR_StoreContainsUnsupportedStatement);
return FALSE;
}
fk->cxStoreArray++; // increment now, because GetXString refers to stores
if (i > 0) {
@ -1526,6 +1532,28 @@ KMX_DWORD CheckStatementOffsets(PFILE_KEYBOARD fk, PFILE_GROUP gp, PKMX_WCHAR co
return STATUS_Success;
}
/**
* Stores can contain only characters, deadkeys, and virtual keys (and `outs`).
* Note that `outs()` is expanded during read so there is no `CODE_OUTS`.
*
* @param store
* @return KMX_BOOL
*/
KMX_BOOL DoesStoreContainInvalidStatement(PKMX_WCHAR store) {
for (PKMX_WCHAR p = store; *p; p = incxstr(p)) {
if (*p == UC_SENTINEL) {
auto code = *(p + 1);
if(code == CODE_DEADKEY || code == CODE_EXTENDED) {
continue;
}
// No other codes are permitted; note, outs() does not have a code as it is expanded at compile time
return TRUE;
}
}
return FALSE;
}
/**
* Checks that the order of statements in the context matches the specification.
* Rule structure: [context] ['+' key] '>' output

View file

@ -9,7 +9,7 @@
set -eu
find "$1" -name '*.kmn' | \
grep -E '(release|experimental)/([a-z0-9_]+)/([a-z0-9_]+)/source/\3\.kmn$' | \
grep -vE 'masaram_gondi|anii|sil_kmhmu|fv_statimcets|fv_nuucaanul|basic_kbdcherp|basic_kbdolch|sil_yi|gff_geez'
grep -vE 'masaram_gondi|anii|sil_kmhmu|fv_statimcets|fv_nuucaanul|basic_kbdcherp|basic_kbdolch|sil_yi|gff_geez|clavbur9|hausa_ajami_qwerty|sil_cameroon_qwerty|sil_cameroon_azerty|sil_dzongkha|sil_yoruba8|sil_yoruba_bar|sil_yoruba_dot'
# #12623: exclude masaram_gondi due to #11806
# #12631: exclude anii, sil_kmhmu as ico references have mismatching case
# #12631: exclude fv_statimcets, fv_nuucaanul as these include U+2002 which is not
@ -18,4 +18,7 @@ find "$1" -name '*.kmn' | \
# whitespace, see also issue #12307
# #14746: exclude sil_yi, as this has whitespace in group name 'Unicode Group'
# #16717: exclude gff_geez, as this has vkeys referenced in store in an any()
# statement
# statement
# #16718: exclude clavbur9, hausa_ajami_qwerty, sil_cameroon_qwerty,
# sil_cameroon_azerty, sil_dzongkha, sil_yoruba8, sil_yoruba_bar,
# sil_yoruba_dot due to use of `nul` or `beep` in stores