Skip to content

Bug Report: implements keyword always tokenized as identifier — OOP interface contracts silently broken #21

Description

@bastitva0-blip

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+)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions