feat(syntax): tokenize $var and @{uuid} kinds + corpus cases
This commit is contained in:
@@ -9,8 +9,15 @@ var + ref, Task 5 adds dp_brace + dp_pipe + dp_multi.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
|
||||
from engine.syntax.types import Token, TokenKind
|
||||
|
||||
# `$name` — identifier starts with letter or underscore, then word chars
|
||||
_VAR_RE = re.compile(r"\$([A-Za-z_][A-Za-z0-9_]*)")
|
||||
# `@{8hex}` — exactly 8 lowercase hex chars in braces
|
||||
_REF_RE = re.compile(r"@\{([0-9a-f]{8})\}")
|
||||
|
||||
|
||||
def tokenize_text(text: str) -> list[Token]:
|
||||
"""Tokenize `text` into a flat list of Token. Single-pass, O(n)."""
|
||||
@@ -64,6 +71,37 @@ def tokenize_text(text: str) -> list[Token]:
|
||||
i += 2
|
||||
continue
|
||||
|
||||
# Variable: $name (only when followed by a valid identifier start)
|
||||
if ch == "$":
|
||||
m = _VAR_RE.match(text, i)
|
||||
if m:
|
||||
_flush_text(i)
|
||||
out.append(Token(
|
||||
kind=TokenKind.VAR,
|
||||
raw=m.group(0),
|
||||
start=i,
|
||||
end=m.end(),
|
||||
meta={"name": m.group(1)},
|
||||
))
|
||||
i = m.end()
|
||||
continue
|
||||
# Lone $ followed by non-ident — fall through to text accumulation
|
||||
|
||||
# Ref: @{8hex} (only when exactly 8 lowercase hex chars in braces)
|
||||
if ch == "@":
|
||||
m = _REF_RE.match(text, i)
|
||||
if m:
|
||||
_flush_text(i)
|
||||
out.append(Token(
|
||||
kind=TokenKind.REF,
|
||||
raw=m.group(0),
|
||||
start=i,
|
||||
end=m.end(),
|
||||
meta={"uuid": m.group(1)},
|
||||
))
|
||||
i = m.end()
|
||||
continue
|
||||
|
||||
# Default: accumulate literal text
|
||||
if text_start is None:
|
||||
text_start = i
|
||||
|
||||
Vendored
+74
@@ -31,6 +31,80 @@
|
||||
{"kind": "escape", "raw": "@@", "start": 4, "end": 6, "meta": {"literal": "@"}},
|
||||
{"kind": "text", "raw": "example", "start": 6, "end": 13, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "single_var",
|
||||
"input": "$style",
|
||||
"expected_tokens": [
|
||||
{"kind": "var", "raw": "$style", "start": 0, "end": 6, "meta": {"name": "style"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "var_in_text",
|
||||
"input": "a $color cat",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "a ", "start": 0, "end": 2, "meta": {}},
|
||||
{"kind": "var", "raw": "$color", "start": 2, "end": 8, "meta": {"name": "color"}},
|
||||
{"kind": "text", "raw": " cat", "start": 8, "end": 12, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "var_with_underscore_and_digits",
|
||||
"input": "$hair_color_2",
|
||||
"expected_tokens": [
|
||||
{"kind": "var", "raw": "$hair_color_2", "start": 0, "end": 13, "meta": {"name": "hair_color_2"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "lone_dollar_is_literal_text",
|
||||
"input": "cost: $",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "cost: $", "start": 0, "end": 7, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "dollar_followed_by_non_ident_is_literal_text",
|
||||
"input": "$ space",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "$ space", "start": 0, "end": 7, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "single_ref",
|
||||
"input": "@{a4f7b2e1}",
|
||||
"expected_tokens": [
|
||||
{"kind": "ref", "raw": "@{a4f7b2e1}", "start": 0, "end": 11, "meta": {"uuid": "a4f7b2e1"}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ref_in_text",
|
||||
"input": "x @{deadbeef} y",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "x ", "start": 0, "end": 2, "meta": {}},
|
||||
{"kind": "ref", "raw": "@{deadbeef}", "start": 2, "end": 13, "meta": {"uuid": "deadbeef"}},
|
||||
{"kind": "text", "raw": " y", "start": 13, "end": 15, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "at_without_braces_is_literal",
|
||||
"input": "email@host",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "email@host", "start": 0, "end": 10, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "at_brace_non_hex_is_literal",
|
||||
"input": "@{not_uuid}",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "@{not_uuid}", "start": 0, "end": 11, "meta": {}}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "at_brace_wrong_length_is_literal",
|
||||
"input": "@{abc} short",
|
||||
"expected_tokens": [
|
||||
{"kind": "text", "raw": "@{abc} short", "start": 0, "end": 12, "meta": {}}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -51,3 +51,17 @@ def test_tokenize_lossless_concatenation_invariant():
|
||||
toks = tokenize_text(s)
|
||||
joined = "".join(t.raw for t in toks)
|
||||
assert joined == s, f"lossless invariant broken for {s!r}: got {joined!r}"
|
||||
|
||||
|
||||
def test_tokenize_lossless_for_var_ref():
|
||||
samples = [
|
||||
"$x",
|
||||
"$x $y",
|
||||
"@{deadbeef} @{cafebabe}",
|
||||
"mix $x @{12345678} text",
|
||||
"lone $ and lone @ stay text",
|
||||
]
|
||||
for s in samples:
|
||||
toks = tokenize_text(s)
|
||||
joined = "".join(t.raw for t in toks)
|
||||
assert joined == s, f"lossless invariant broken for {s!r}: got {joined!r}"
|
||||
|
||||
Reference in New Issue
Block a user