Skip to content

Commit 3ea6fa5

Browse files
authored
Add tests for IX lexical vocabulary and token records
This file contains tests for the IX lexical vocabulary and token records, ensuring correct behavior of keywords, identifiers, and literal tokens.
1 parent 790f477 commit 3ea6fa5

1 file changed

Lines changed: 173 additions & 0 deletions

File tree

tests/language/test_tokens.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
"""Tests for IX lexical vocabulary and token records."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from ix_sally.foundation import FoundationError
8+
from ix_sally.language.source import SourcePosition, SourceSpan
9+
from ix_sally.language.tokens import (
10+
KEYWORDS_BY_LEXEME,
11+
Keyword,
12+
LanguageToken,
13+
TokenKind,
14+
)
15+
16+
17+
def _span(text: str = "token") -> SourceSpan:
18+
"""Return a deterministic source span for one token lexeme."""
19+
return SourceSpan.covering(
20+
filename="program.ix",
21+
start=SourcePosition.start(),
22+
text=text,
23+
)
24+
25+
26+
def test_keyword_vocabulary_maps_every_reserved_lexeme() -> None:
27+
"""Every reserved word must have one deterministic lookup entry."""
28+
assert KEYWORDS_BY_LEXEME == {
29+
keyword.value: keyword for keyword in Keyword
30+
}
31+
assert KEYWORDS_BY_LEXEME["human_approval"] is Keyword.HUMAN_APPROVAL
32+
assert KEYWORDS_BY_LEXEME["claim_boundary"] is Keyword.CLAIM_BOUNDARY
33+
34+
35+
def test_keyword_or_identifier_classifies_reserved_words() -> None:
36+
"""Reserved words and user identifiers must produce distinct token kinds."""
37+
reserved = LanguageToken.keyword_or_identifier(
38+
lexeme="remember",
39+
span=_span("remember"),
40+
)
41+
identifier = LanguageToken.keyword_or_identifier(
42+
lexeme="memory_key",
43+
span=_span("memory_key"),
44+
)
45+
46+
assert reserved.kind is TokenKind.KEYWORD
47+
assert reserved.keyword is Keyword.REMEMBER
48+
assert reserved.is_keyword(Keyword.REMEMBER) is True
49+
assert identifier.kind is TokenKind.IDENTIFIER
50+
assert identifier.keyword is None
51+
52+
53+
def test_literal_keywords_preserve_typed_values() -> None:
54+
"""Boolean and null keywords must carry their semantic values."""
55+
true_token = LanguageToken.keyword_or_identifier(
56+
lexeme="true",
57+
span=_span("true"),
58+
)
59+
false_token = LanguageToken.keyword_or_identifier(
60+
lexeme="false",
61+
span=_span("false"),
62+
)
63+
null_token = LanguageToken.keyword_or_identifier(
64+
lexeme="null",
65+
span=_span("null"),
66+
)
67+
68+
assert true_token.literal is True
69+
assert false_token.literal is False
70+
assert null_token.literal is None
71+
assert true_token.has_literal() is True
72+
assert false_token.has_literal() is True
73+
assert null_token.has_literal() is True
74+
75+
76+
def test_literal_token_payload_and_digest_are_deterministic() -> None:
77+
"""Equivalent lexical records must produce identical receipt digests."""
78+
first = LanguageToken(
79+
kind=TokenKind.INTEGER,
80+
lexeme="42",
81+
span=_span("42"),
82+
literal=42,
83+
)
84+
second = LanguageToken(
85+
kind=TokenKind.INTEGER,
86+
lexeme="42",
87+
span=_span("42"),
88+
literal=42,
89+
)
90+
91+
assert first.to_payload()["has_literal"] is True
92+
assert first.to_payload()["literal"] == 42
93+
assert first.digest() == second.digest()
94+
95+
96+
def test_end_of_file_token_requires_zero_width_span() -> None:
97+
"""End-of-file tokens must contain no lexeme and consume no source text."""
98+
eof_span = SourceSpan.point(
99+
filename="program.ix",
100+
line=4,
101+
column=2,
102+
offset=27,
103+
)
104+
105+
token = LanguageToken.end_of_file(span=eof_span)
106+
107+
assert token.kind is TokenKind.EOF
108+
assert token.lexeme == ""
109+
assert token.span == eof_span
110+
111+
with pytest.raises(
112+
FoundationError,
113+
match="span must be zero-width",
114+
):
115+
LanguageToken(
116+
kind=TokenKind.EOF,
117+
lexeme="",
118+
span=_span("x"),
119+
)
120+
121+
122+
def test_keyword_token_rejects_mismatched_metadata() -> None:
123+
"""Keyword records must not misrepresent their source lexeme."""
124+
with pytest.raises(
125+
FoundationError,
126+
match="lexeme must match",
127+
):
128+
LanguageToken(
129+
kind=TokenKind.KEYWORD,
130+
lexeme="allow",
131+
span=_span("allow"),
132+
keyword=Keyword.DENY,
133+
)
134+
135+
136+
def test_literal_token_rejects_wrong_python_type() -> None:
137+
"""Literal token kinds must retain exact runtime value types."""
138+
with pytest.raises(
139+
FoundationError,
140+
match="integer token literal must be an integer",
141+
):
142+
LanguageToken(
143+
kind=TokenKind.INTEGER,
144+
lexeme="true",
145+
span=_span("true"),
146+
literal=True,
147+
)
148+
149+
with pytest.raises(
150+
FoundationError,
151+
match="incorrect literal value",
152+
):
153+
LanguageToken(
154+
kind=TokenKind.KEYWORD,
155+
lexeme="false",
156+
span=_span("false"),
157+
literal=True,
158+
keyword=Keyword.FALSE,
159+
)
160+
161+
162+
def test_structural_token_rejects_literal_value() -> None:
163+
"""Punctuation and structural tokens must not carry runtime values."""
164+
with pytest.raises(
165+
FoundationError,
166+
match="left_brace token must not carry a literal value",
167+
):
168+
LanguageToken(
169+
kind=TokenKind.LEFT_BRACE,
170+
lexeme="{",
171+
span=_span("{"),
172+
literal="unexpected",
173+
)

0 commit comments

Comments
 (0)