3333# GitHub raw content URL template
3434KIWI_TYPES_H_URL = "https://raw.githubusercontent.com/bab2min/Kiwi/{version}/include/kiwi/Types.h"
3535KIWI_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.
3839SKIPPED_ENUMERATORS = {
4445# Bases whose `<base>i` spelling maps to a POS_<BASE>_I constant.
4546IRREGULAR_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.
4956COMPAT_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+
160182def 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
189215def 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 ("\t switch p {" )
262313 lines .append ("\t case" )
@@ -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 ("\n Extracted 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
355422if __name__ == "__main__" :
0 commit comments