Bug Report: implements keyword always tokenized as identifier — OOP interface contracts silently broken
Labels: bug · scanner · lexer · OOP · good-first-issue
Component: src/compiler/lexer/scanner.c
Version: 1.3.3
Severity: High — a documented language keyword silently does nothing
Describe the Bug
The implements keyword is completely non-functional. Any class that tries to implement an interface using implements compiles without error but silently ignores the keyword — the parser never sees TOKEN_IMPLEMENTS, it sees TOKEN_IDENTIFIER instead, and the interface contract is never enforced.
This is caused by two back-to-back mistakes in scanner.c inside identifierType():
// Current broken code (~line 362)
case 'i':
if (scanner->current - scanner->start > 2 && scanner->start[2] == 'l')
return checkKeyword(scanner, 4, 7, "ements", TOKEN_IMPLEMENTS);
break;
Mistake 1 — Wrong character check:
implements is spelled i-m-p-l-e-m-e-n-t-s. Index 2 is 'p', not 'l'. The condition start[2] == 'l' is never true for this word, so the branch is permanently dead.
Mistake 2 — Wrong length argument:
Even if the character check were fixed, checkKeyword(scanner, 4, 7, "ements") passes 7 as the length but "ements" is only 6 characters. This would cause checkKeyword to read one byte past the string literal — always failing the memcmp.
The parser has correct handling ready and waiting at line 471:
if (match(p, 1, TOKEN_IMPLEMENTS)) {
// ... interface list parsing
}
It just never fires because the scanner never produces TOKEN_IMPLEMENTS.
To Reproduce
Create interface_test.prox:
interface Greetable {
func greet();
}
class Person implements Greetable {
func greet() {
print("Hello!");
}
}
let p = new Person();
p.greet();
Run it:
./proxpl interface_test.prox
Expected: Class Person is registered as implementing Greetable; interface contract is enforced.
Actual: implements is parsed as an identifier named implements. The parser then hits { when it expects either { or implements, which either errors with a confusing message or skips the contract entirely depending on parser state.
Root Cause
In src/compiler/lexer/scanner.c, inside the identifierType() trie, the 'm' branch under case 'i' has the wrong index check and wrong length:
case 'm':
if (scanner->current - scanner->start > 2 && scanner->start[2] == 'l') // ← 'l' should be 'p'
return checkKeyword(scanner, 4, 7, "ements", TOKEN_IMPLEMENTS); // ← 7 should be 6
break;
Expected Behavior
implements should produce TOKEN_IMPLEMENTS, allowing class declarations to correctly bind interface contracts.
Fix
Two characters need to change in scanner.c:
// Before (broken)
case 'm':
if (scanner->current - scanner->start > 2 && scanner->start[2] == 'l')
return checkKeyword(scanner, 4, 7, "ements", TOKEN_IMPLEMENTS);
break;
// After (correct)
case 'm':
if (scanner->current - scanner->start > 3 &&
scanner->start[2] == 'p' && scanner->start[3] == 'l')
return checkKeyword(scanner, 4, 6, "ements", TOKEN_IMPLEMENTS);
break;
Why > 3 and two checks:
We need to confirm both start[2] == 'p' and start[3] == 'l' before delegating to checkKeyword, so the length guard needs to be > 3 (at least 4 chars seen: i, m, p, l). Then checkKeyword(4, 6, "ements") confirms the remaining 6 characters e-m-e-n-t-s, giving a total of 10 — exactly implements.
Additional Context
While reviewing the scanner I also found three related issues that may warrant separate reports:
import keyword missing from scanner — Listed as a reserved keyword in SPEC.md but has no TOKEN_IMPORT or trie entry in scanner.c. Currently tokenizes as TOKEN_IDENTIFIER.
- Hex (
0x) and binary (0b) literals not implemented — SPEC.md documents 0x7B and 0b1111011 as valid integer literal formats. The number() function in scanner.c only handles decimal and float — no prefix handling exists for either format.
- Dead
TOKEN_DECAY branch — Under case 'd' → start[1]='e' → start[2]='a', there is an unreachable branch returning TOKEN_DECAY. decay is d-e-c-a-y so start[2] is 'c', not 'a'. This is harmless dead code but suggests a copy-paste error worth cleaning up.
Happy to submit a PR with the fix for the implements bug. It's a one-line change in the scanner.
ProXPL Version: 1.3.3
OS: (tested on Linux / Windows)
Built from: source (gcc, CMake 3.15+)
Bug Report:
implementskeyword always tokenized as identifier — OOP interface contracts silently brokenLabels:
bug·scanner·lexer·OOP·good-first-issueComponent:
src/compiler/lexer/scanner.cVersion: 1.3.3
Severity: High — a documented language keyword silently does nothing
Describe the Bug
The
implementskeyword is completely non-functional. Any class that tries to implement an interface usingimplementscompiles without error but silently ignores the keyword — the parser never seesTOKEN_IMPLEMENTS, it seesTOKEN_IDENTIFIERinstead, and the interface contract is never enforced.This is caused by two back-to-back mistakes in
scanner.cinsideidentifierType():Mistake 1 — Wrong character check:
implementsis spelledi-m-p-l-e-m-e-n-t-s. Index 2 is'p', not'l'. The conditionstart[2] == 'l'is never true for this word, so the branch is permanently dead.Mistake 2 — Wrong length argument:
Even if the character check were fixed,
checkKeyword(scanner, 4, 7, "ements")passes7as the length but"ements"is only 6 characters. This would causecheckKeywordto read one byte past the string literal — always failing thememcmp.The parser has correct handling ready and waiting at line 471:
It just never fires because the scanner never produces
TOKEN_IMPLEMENTS.To Reproduce
Create
interface_test.prox:Run it:
Expected: Class
Personis registered as implementingGreetable; interface contract is enforced.Actual:
implementsis parsed as an identifier namedimplements. The parser then hits{when it expects either{orimplements, which either errors with a confusing message or skips the contract entirely depending on parser state.Root Cause
In
src/compiler/lexer/scanner.c, inside theidentifierType()trie, the'm'branch undercase 'i'has the wrong index check and wrong length:Expected Behavior
implementsshould produceTOKEN_IMPLEMENTS, allowing class declarations to correctly bind interface contracts.Fix
Two characters need to change in
scanner.c:Why
> 3and two checks:We need to confirm both
start[2] == 'p'andstart[3] == 'l'before delegating tocheckKeyword, so the length guard needs to be> 3(at least 4 chars seen:i,m,p,l). ThencheckKeyword(4, 6, "ements")confirms the remaining 6 characterse-m-e-n-t-s, giving a total of 10 — exactlyimplements.Additional Context
While reviewing the scanner I also found three related issues that may warrant separate reports:
importkeyword missing from scanner — Listed as a reserved keyword in SPEC.md but has noTOKEN_IMPORTor trie entry inscanner.c. Currently tokenizes asTOKEN_IDENTIFIER.0x) and binary (0b) literals not implemented — SPEC.md documents0x7Band0b1111011as valid integer literal formats. Thenumber()function inscanner.conly handles decimal and float — no prefix handling exists for either format.TOKEN_DECAYbranch — Undercase 'd'→start[1]='e'→start[2]='a', there is an unreachable branch returningTOKEN_DECAY.decayisd-e-c-a-ysostart[2]is'c', not'a'. This is harmless dead code but suggests a copy-paste error worth cleaning up.Happy to submit a PR with the fix for the
implementsbug. It's a one-line change in the scanner.ProXPL Version: 1.3.3
OS: (tested on Linux / Windows)
Built from: source (
gcc, CMake 3.15+)