Skip to content

Commit ba165a3

Browse files
committed
fix: mirror upstream toPOSTag for the accepted tag set
ParsePOSType is the Go counterpart of upstream's toPOSTag, so derive the accepted strings from that function instead of inventing them. The previous commit mapped POS_PA, POS_PV_I and POS_PA_I to "@". That string is not something upstream ever converts to or from: tagToString guards its table with assert(t < POSTag::max) and pa == max, so reading "@" only happens in an NDEBUG build past the assertion, and toPOSTag has no entry for "@" at all. Drop the three constants rather than exposing a sentinel as a tag. Restore POS_V and add the remaining strings toPOSTag accepts but Kiwi never emits: A (POSTag::p), NF, NV, NA and UNK (POSTag::unknown), and ^ (POSTag::unknown, spelled POS_CARET). POS_V had been removed on the grounds that Kiwi never returns "V", which is true of tagToString but irrelevant to a parser -- toPOSTag accepts it. kiwigo now accepts exactly toPOSTag's 75 strings plus "P", which tagToString can return for POSTag::p even though toPOSTag does not accept it. Emit the two groups as separate blocks so the generated alignment matches gofmt.
1 parent d8e58b4 commit ba165a3

3 files changed

Lines changed: 135 additions & 41 deletions

File tree

postype.go

Lines changed: 17 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

postype_test.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,27 @@ func TestParsePOSType(t *testing.T) {
4545
want: POS_VV_I,
4646
wantErr: false,
4747
},
48+
// toPOSTag accepts these as input even though Kiwi never emits them.
4849
{
49-
name: "@ is a POSType",
50-
arg: "@",
51-
want: POS_PA,
50+
name: "V is a POSType",
51+
arg: "V",
52+
want: POS_V,
5253
wantErr: false,
5354
},
55+
{
56+
name: "UNK is a POSType",
57+
arg: "UNK",
58+
want: POS_UNK,
59+
wantErr: false,
60+
},
61+
// toPOSTag has no entry for "@"; it is the sentinel tagToString falls
62+
// back to for values it is never meant to be called with.
63+
{
64+
name: "@ is not a valid POSType",
65+
arg: "@",
66+
want: POS_UNKNOWN,
67+
wantErr: true,
68+
},
5469
}
5570
for _, tt := range tests {
5671
t.Run(tt.name, func(t *testing.T) {

scripts/extract_postags.py

Lines changed: 100 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
# GitHub raw content URL template
3434
KIWI_TYPES_H_URL = "https://raw.githubusercontent.com/bab2min/Kiwi/{version}/include/kiwi/Types.h"
3535
KIWI_UTILS_CPP_URL = "https://raw.githubusercontent.com/bab2min/Kiwi/{version}/src/Utils.cpp"
36+
KIWI_STRUTILS_H_URL = "https://raw.githubusercontent.com/bab2min/Kiwi/{version}/src/StrUtils.h"
3637

3738
# Enumerators that are not POS tags themselves.
3839
SKIPPED_ENUMERATORS = {
@@ -44,6 +45,12 @@
4445
# Bases whose `<base>i` spelling maps to a POS_<BASE>_I constant.
4546
IRREGULAR_BASES = ("vv", "va", "vx", "xsa", "pv", "pa")
4647

48+
# Go constant names for tag strings that toPOSTag accepts but that are not
49+
# spelled like an identifier.
50+
PUNCTUATION_TAG_NAMES = {
51+
"^": "POS_CARET",
52+
}
53+
4754
# Backwards-compatible aliases kept for downstream code. These cannot be derived
4855
# from the C++ source; they exist because kiwigo used to spell them this way.
4956
COMPAT_ALIASES = [
@@ -157,18 +164,37 @@ def extract_case_returns(body: str) -> dict[str, str]:
157164
return dict(re.findall(pattern, body))
158165

159166

167+
def extract_accepted_tags(body: str) -> list[str]:
168+
"""Extract the tag strings that `toPOSTag` accepts, in source order.
169+
170+
toPOSTag is the upstream counterpart of ParsePOSType, so it defines which
171+
strings are valid input. Anything it does not recognise falls through to
172+
`return POSTag::max`, which is upstream's way of rejecting the string.
173+
"""
174+
pattern = r'tagStr\s*==\s*u"([^"]*)"\s*\)\s*return'
175+
seen: list[str] = []
176+
for text in re.findall(pattern, body):
177+
if text not in seen:
178+
seen.append(text)
179+
return seen
180+
181+
160182
def make_tag_to_string(tag_strings: list[str], irregular_cases: dict[str, str],
161-
values: dict[str, int], irregular_flag: int):
162-
"""Build a Python transcription of the C++ `tagToString`."""
183+
values: dict[str, int], irregular_flag: int, max_value: int):
184+
"""Build a Python transcription of the C++ `tagToString`.
185+
186+
Returns None where upstream has no tag string for the value. tagToString
187+
guards its table lookup with `assert(t < POSTag::max)` and its irregular
188+
branch falls through to a "@" sentinel, so those values are not tags that
189+
upstream ever converts; `toPOSTag` rejects "@" as well.
190+
"""
163191
# Map the numeric value of each irregular case label to its return string.
164192
irregular_by_value = {values[name]: text for name, text in irregular_cases.items()}
165-
default_irregular = "@"
166193

167194
def tag_to_string(value: int) -> str | None:
168195
if value & irregular_flag:
169-
cleared = value & ~irregular_flag
170-
return irregular_by_value.get(cleared, default_irregular)
171-
if value >= len(tag_strings):
196+
return irregular_by_value.get(value & ~irregular_flag)
197+
if value >= max_value:
172198
return None
173199
return tag_strings[value]
174200

@@ -187,10 +213,23 @@ def go_constant_name(cpp_name: str) -> str:
187213

188214

189215
def build_tag_entries(tags: list[dict], values: dict[str, int], tag_to_string,
190-
regular_cases: dict[str, str]) -> list[tuple[str, str]]:
191-
"""Build the ordered (go_name, go_value) list for the generated constants."""
192-
entries: list[tuple[str, str]] = []
216+
regular_cases: dict[str, str],
217+
accepted: list[str]) -> tuple[list[tuple[str, str]], list[tuple[str, str]]]:
218+
"""Build the (go_name, go_value) lists for the generated constants.
219+
220+
Returns the tags Kiwi can emit and, separately, the input-only aliases that
221+
upstream's toPOSTag accepts but never produces.
222+
"""
223+
emitted: list[tuple[str, str]] = []
193224
seen_names: set[str] = set()
225+
seen_values: set[str] = set()
226+
227+
def add(go_name: str, go_value: str, target: list[tuple[str, str]]) -> None:
228+
if go_name in seen_names:
229+
return
230+
seen_names.add(go_name)
231+
seen_values.add(go_value)
232+
target.append((go_name, go_value))
194233

195234
for tag in tags:
196235
cpp_name = tag["name"]
@@ -201,27 +240,29 @@ def build_tag_entries(tags: list[dict], values: dict[str, int], tag_to_string,
201240
if go_value is None:
202241
continue
203242

204-
go_name = go_constant_name(cpp_name)
205-
if go_name in seen_names:
206-
continue
207-
208-
seen_names.add(go_name)
209-
entries.append((go_name, go_value))
243+
add(go_constant_name(cpp_name), go_value, emitted)
210244

211245
# `-R` variants are only reachable through tagRToString, so they are not
212246
# derivable from the enum alone.
213247
for cpp_name, text in regular_cases.items():
214-
go_name = f"POS_{cpp_name.upper()}_R"
215-
if go_name in seen_names:
248+
add(f"POS_{cpp_name.upper()}_R", text, emitted)
249+
250+
# Strings toPOSTag accepts as input but tagToString never returns, such as
251+
# "V" and "A" for POSTag::p or "NF"/"NV"/"NA"/"UNK" for POSTag::unknown.
252+
aliases: list[tuple[str, str]] = []
253+
for text in accepted:
254+
if text in seen_values:
216255
continue
217-
seen_names.add(go_name)
218-
entries.append((go_name, text))
256+
go_name = PUNCTUATION_TAG_NAMES.get(text, f"POS_{text.upper()}")
257+
add(go_name, text, aliases)
219258

220-
return entries
259+
return emitted, aliases
221260

222261

223-
def generate_go_file(entries: list[tuple[str, str]], version: str) -> str:
262+
def generate_go_file(emitted: list[tuple[str, str]], aliases: list[tuple[str, str]],
263+
version: str) -> str:
224264
"""Generate Go source file with POS type definitions."""
265+
entries = emitted + aliases
225266
lines = []
226267
lines.append("// Code generated by scripts/extract_postags.py; DO NOT EDIT.")
227268
lines.append(f"// Source: Kiwi {version}")
@@ -236,11 +277,19 @@ def generate_go_file(entries: list[tuple[str, str]], version: str) -> str:
236277
lines.append("")
237278
lines.append("const (")
238279

239-
max_name_len = max(len(name) for name, _ in entries)
280+
# gofmt aligns each run of declarations separated by a blank line on its
281+
# own, so each group is padded to its own longest name.
282+
def declare_group(group: list[tuple[str, str]]) -> list[str]:
283+
width = max(len(name) for name, _ in group)
284+
return [
285+
f'\t{go_name}{" " * (width - len(go_name) + 1)}POSType = "{go_value}"'
286+
for go_name, go_value in group
287+
]
240288

241-
for go_name, go_value in entries:
242-
padding = " " * (max_name_len - len(go_name) + 1)
243-
lines.append(f'\t{go_name}{padding}POSType = "{go_value}"')
289+
lines.extend(declare_group(emitted))
290+
lines.append("")
291+
lines.append("\t// Accepted by toPOSTag but never produced by Kiwi itself.")
292+
lines.extend(declare_group(aliases))
244293

245294
lines.append(")")
246295
lines.append("")
@@ -255,8 +304,10 @@ def generate_go_file(entries: list[tuple[str, str]], version: str) -> str:
255304
lines.append(")")
256305
lines.append("")
257306

258-
# Generate isValid function. A Go switch requires distinct case values, and
259-
# several constants are aliases sharing one string, so deduplicate by value.
307+
# Generate isValid function. This mirrors which strings upstream's toPOSTag
308+
# accepts, plus the strings tagToString/tagRToString can return. A Go switch
309+
# requires distinct case values, and several constants are aliases sharing
310+
# one string, so deduplicate by value.
260311
lines.append("func (p POSType) isValid() bool {")
261312
lines.append("\tswitch p {")
262313
lines.append("\tcase")
@@ -305,6 +356,10 @@ def main():
305356
print(f"Fetching {utils_url}")
306357
utils_source = fetch_file(utils_url)
307358

359+
strutils_url = KIWI_STRUTILS_H_URL.format(version=version)
360+
print(f"Fetching {strutils_url}")
361+
strutils_source = fetch_file(strutils_url)
362+
308363
# Extract enum values
309364
print("Parsing POSTag enum...")
310365
tags = extract_enum_values(types_source, "POSTag")
@@ -328,11 +383,21 @@ def main():
328383
regular_cases = extract_case_returns(tag_r_to_string_body)
329384
print(f"Found {len(regular_cases)} regular-conjugation tags")
330385

331-
tag_to_string = make_tag_to_string(tag_strings, irregular_cases, values, values["irregular"])
332-
entries = build_tag_entries(tags, values, tag_to_string, regular_cases)
386+
# toPOSTag defines which strings upstream accepts as input, which is the
387+
# role ParsePOSType plays on the Go side.
388+
to_pos_tag_body = extract_function_body(strutils_source, r"inline\s+POSTag\s+toPOSTag\s*\(\s*std::u16string_view")
389+
accepted = extract_accepted_tags(to_pos_tag_body)
390+
if not accepted:
391+
raise SystemExit("toPOSTag not found; cannot determine accepted tag strings")
392+
print(f"Found {len(accepted)} accepted tag strings")
393+
394+
tag_to_string = make_tag_to_string(
395+
tag_strings, irregular_cases, values, values["irregular"], values["max"]
396+
)
397+
emitted, aliases = build_tag_entries(tags, values, tag_to_string, regular_cases, accepted)
333398

334399
# Generate Go file
335-
go_content = generate_go_file(entries, version)
400+
go_content = generate_go_file(emitted, aliases, version)
336401

337402
# Write output
338403
output_path = Path("postype_generated.go")
@@ -343,13 +408,15 @@ def main():
343408
print("\nExtracted tags:")
344409
for tag in tags:
345410
cpp_name = tag["name"]
346-
if cpp_name in SKIPPED_ENUMERATORS:
347-
print(f" {cpp_name:20s} -> (skipped)")
411+
text = None if cpp_name in SKIPPED_ENUMERATORS else tag_to_string(values[cpp_name])
412+
if text is None:
413+
print(f" {cpp_name:20s} -> (no tag string; skipped)")
348414
continue
349-
text = tag_to_string(values[cpp_name])
350415
print(f' {cpp_name:20s} -> {go_constant_name(cpp_name):20s} = "{text}"')
351416
for cpp_name, text in regular_cases.items():
352417
print(f' {cpp_name + " (R)":20s} -> {"POS_" + cpp_name.upper() + "_R":20s} = "{text}"')
418+
for go_name, text in aliases:
419+
print(f' {"(toPOSTag only)":20s} -> {go_name:20s} = "{text}"')
353420

354421

355422
if __name__ == "__main__":

0 commit comments

Comments
 (0)