Skip to content

Commit 00fef58

Browse files
authored
Merge pull request rizonesoft#5570 from RaiKoHoff/Dev_Master
chore: upd uthash, add timsort (repl. qsort)
2 parents 4276aa5 + 9efe9b4 commit 00fef58

25 files changed

Lines changed: 2868 additions & 159 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
ehthumbs.db
99
Thumbs.db
1010
.vscode
11+
.venv
1112
.agent
1213

1314
# C++ Junk #

Build/scripts/check_scicall.py

Lines changed: 386 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,386 @@
1+
#!/usr/bin/env python3
2+
"""
3+
check_scicall.py - Diff Scintilla.iface against SciCall.h
4+
5+
Parses Scintilla.iface to find all fun/get/set messages and compares
6+
against SciCall.h to find:
7+
1. iface messages NOT wrapped in SciCall.h (candidates to add)
8+
2. SciCall.h wrappers that don't match any iface message (stale/custom)
9+
10+
Usage:
11+
python Build/scripts/check_scicall.py
12+
python Build/scripts/check_scicall.py --verbose
13+
python Build/scripts/check_scicall.py --category Basics
14+
python Build/scripts/check_scicall.py --generate
15+
"""
16+
17+
import argparse
18+
import os
19+
import re
20+
import sys
21+
22+
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
23+
REPO_ROOT = os.path.normpath(os.path.join(SCRIPT_DIR, "..", ".."))
24+
25+
IFACE_PATH = os.path.join(REPO_ROOT, "scintilla", "include", "Scintilla.iface")
26+
SCICALL_PATH = os.path.join(REPO_ROOT, "src", "SciCall.h")
27+
28+
# iface type -> NP3 C type mapping
29+
TYPE_MAP = {
30+
"void": "void",
31+
"position": "DocPos",
32+
"line": "DocLn",
33+
"colour": "COLORREF",
34+
"colouralpha": "COLORALPHAREF",
35+
"bool": "bool",
36+
"int": "int",
37+
"string": "const char*",
38+
"stringresult": "char*",
39+
"pointer": "sptr_t",
40+
"cells": "const char*",
41+
"textrange": "struct Sci_TextRange*",
42+
"textrangefull": "struct Sci_TextRangeFull*",
43+
"findtext": "struct Sci_TextToFind*",
44+
"findtextfull": "struct Sci_TextToFindFull*",
45+
"formatrange": "struct Sci_RangeToFormat*",
46+
"formatrangefull": "struct Sci_RangeToFormatFull*",
47+
"keymod": "size_t",
48+
}
49+
50+
51+
def map_type(iface_type):
52+
"""Map an iface type to an NP3 C type."""
53+
if iface_type in TYPE_MAP:
54+
return TYPE_MAP[iface_type]
55+
# Capitalized enum types -> int
56+
if iface_type and iface_type[0].isupper():
57+
return "int"
58+
return "int"
59+
60+
61+
def parse_iface(path):
62+
"""Parse Scintilla.iface, returning a dict of uppercase_name -> info."""
63+
messages = {}
64+
current_category = "Unknown"
65+
66+
# Match: fun/get/set <rettype> <Name>=<number>(<params>)
67+
msg_re = re.compile(
68+
r"^(fun|get|set)\s+" # feature type
69+
r"(\S+)\s+" # return type
70+
r"(\w+)" # function name
71+
r"=(\d+)" # message number
72+
r"\(([^)]*)\)" # parameters
73+
)
74+
cat_re = re.compile(r"^cat\s+(\w+)")
75+
76+
with open(path, "r", encoding="utf-8") as f:
77+
for line_no, line in enumerate(f, 1):
78+
line = line.rstrip()
79+
80+
cat_m = cat_re.match(line)
81+
if cat_m:
82+
current_category = cat_m.group(1)
83+
continue
84+
85+
if line.startswith("##") or line.startswith("#!"):
86+
continue
87+
88+
msg_m = msg_re.match(line)
89+
if msg_m:
90+
feat_type = msg_m.group(1)
91+
ret_type = msg_m.group(2)
92+
name = msg_m.group(3)
93+
msg_num = int(msg_m.group(4))
94+
params_str = msg_m.group(5).strip()
95+
upper_name = name.upper()
96+
97+
messages[upper_name] = {
98+
"name": name,
99+
"upper": upper_name,
100+
"type": feat_type,
101+
"ret": ret_type,
102+
"num": msg_num,
103+
"params": params_str,
104+
"category": current_category,
105+
"line": line_no,
106+
"raw": line,
107+
}
108+
109+
return messages
110+
111+
112+
def parse_scicall(path):
113+
"""Parse SciCall.h, returning a dict of uppercase_msg -> info."""
114+
wrappers = {}
115+
116+
# Match: DeclareSciCall{R,V}{0,01,1,2}(fn, MSG, ...)
117+
decl_re = re.compile(
118+
r"DeclareSciCall([RV])(0|01|1|2)\s*\(\s*(\w+)\s*,\s*(\w+)"
119+
)
120+
# Also match commented-out declarations
121+
commented_re = re.compile(
122+
r"//~?\s*DeclareSciCall([RV])(0|01|1|2)\s*\(\s*(\w+)\s*,\s*(\w+)"
123+
)
124+
125+
with open(path, "r", encoding="utf-8") as f:
126+
for line_no, line in enumerate(f, 1):
127+
line_stripped = line.rstrip()
128+
129+
# Skip macro definitions (#define DeclareSciCall...)
130+
if line_stripped.startswith("#define"):
131+
continue
132+
133+
# Check for commented-out declarations (track but mark)
134+
cm = commented_re.match(line_stripped)
135+
if cm:
136+
upper_msg = cm.group(4)
137+
wrappers[upper_msg] = {
138+
"fn": cm.group(3),
139+
"msg": upper_msg,
140+
"macro_ret": cm.group(1),
141+
"macro_params": cm.group(2),
142+
"line": line_no,
143+
"commented": True,
144+
"raw": line_stripped,
145+
}
146+
continue
147+
148+
dm = decl_re.search(line_stripped)
149+
if dm:
150+
upper_msg = dm.group(4)
151+
wrappers[upper_msg] = {
152+
"fn": dm.group(3),
153+
"msg": upper_msg,
154+
"macro_ret": dm.group(1),
155+
"macro_params": dm.group(2),
156+
"line": line_no,
157+
"commented": False,
158+
"raw": line_stripped,
159+
}
160+
161+
return wrappers
162+
163+
164+
def parse_param(param_str):
165+
"""Parse a single iface param like 'position pos' or '' into (type, name)."""
166+
param_str = param_str.strip()
167+
if not param_str:
168+
return None, None
169+
# Handle default values like 'int defaultValue'
170+
parts = param_str.split()
171+
if len(parts) >= 2:
172+
return parts[0], parts[1].split("=")[0]
173+
elif len(parts) == 1:
174+
return parts[0], "param"
175+
return None, None
176+
177+
178+
def generate_wrapper(msg):
179+
"""Generate a DeclareSciCall* line for an iface message."""
180+
name = msg["name"]
181+
upper = msg["upper"]
182+
ret_type = msg["ret"]
183+
params_str = msg["params"]
184+
185+
# Parse return type
186+
c_ret = map_type(ret_type)
187+
is_void = (ret_type == "void")
188+
189+
# Parse parameters
190+
if "," in params_str:
191+
wp_str, lp_str = params_str.split(",", 1)
192+
else:
193+
wp_str = params_str
194+
lp_str = ""
195+
196+
wp_type, wp_name = parse_param(wp_str)
197+
lp_type, lp_name = parse_param(lp_str)
198+
199+
has_wp = wp_type is not None
200+
has_lp = lp_type is not None
201+
202+
# Determine macro variant
203+
rv = "V" if is_void else "R"
204+
205+
if has_wp and has_lp:
206+
variant = "2"
207+
elif has_wp and not has_lp:
208+
variant = "1"
209+
elif not has_wp and has_lp:
210+
variant = "01"
211+
else:
212+
variant = "0"
213+
214+
macro = f"DeclareSciCall{rv}{variant}"
215+
216+
# Build arguments
217+
if variant == "0":
218+
if is_void:
219+
return f"{macro}({name}, {upper});"
220+
else:
221+
return f"{macro}({name}, {upper}, {c_ret});"
222+
elif variant == "1":
223+
c_wp = map_type(wp_type)
224+
if is_void:
225+
return f"{macro}({name}, {upper}, {c_wp}, {wp_name});"
226+
else:
227+
return f"{macro}({name}, {upper}, {c_ret}, {c_wp}, {wp_name});"
228+
elif variant == "01":
229+
c_lp = map_type(lp_type)
230+
if is_void:
231+
return f"{macro}({name}, {upper}, {c_lp}, {lp_name});"
232+
else:
233+
return f"{macro}({name}, {upper}, {c_ret}, {c_lp}, {lp_name});"
234+
elif variant == "2":
235+
c_wp = map_type(wp_type)
236+
c_lp = map_type(lp_type)
237+
if is_void:
238+
return f"{macro}({name}, {upper}, {c_wp}, {wp_name}, {c_lp}, {lp_name});"
239+
else:
240+
return f"{macro}({name}, {upper}, {c_ret}, {c_wp}, {wp_name}, {c_lp}, {lp_name});"
241+
242+
return f"// TODO: {name}"
243+
244+
245+
def main():
246+
parser = argparse.ArgumentParser(
247+
description="Diff Scintilla.iface against SciCall.h to find unwrapped messages"
248+
)
249+
parser.add_argument(
250+
"--verbose", "-v", action="store_true",
251+
help="Show detailed info for each unwrapped message"
252+
)
253+
parser.add_argument(
254+
"--category", "-c", type=str, default=None,
255+
help="Filter to a specific iface category (e.g. Basics, Provisional, Deprecated)"
256+
)
257+
parser.add_argument(
258+
"--show-wrapped", "-w", action="store_true",
259+
help="Also list messages that ARE wrapped (for completeness check)"
260+
)
261+
parser.add_argument(
262+
"--generate", "-g", action="store_true",
263+
help="Generate DeclareSciCall* lines for all unwrapped non-deprecated messages"
264+
)
265+
parser.add_argument(
266+
"--iface", type=str, default=IFACE_PATH,
267+
help=f"Path to Scintilla.iface (default: {IFACE_PATH})"
268+
)
269+
parser.add_argument(
270+
"--scicall", type=str, default=SCICALL_PATH,
271+
help=f"Path to SciCall.h (default: {SCICALL_PATH})"
272+
)
273+
args = parser.parse_args()
274+
275+
if not os.path.isfile(args.iface):
276+
print(f"Error: {args.iface} not found", file=sys.stderr)
277+
sys.exit(1)
278+
if not os.path.isfile(args.scicall):
279+
print(f"Error: {args.scicall} not found", file=sys.stderr)
280+
sys.exit(1)
281+
282+
all_iface_msgs = parse_iface(args.iface)
283+
scicall_wrappers = parse_scicall(args.scicall)
284+
285+
# Apply category filter for unwrapped/wrapped analysis
286+
if args.category:
287+
iface_msgs = {
288+
k: v for k, v in all_iface_msgs.items()
289+
if v["category"].lower() == args.category.lower()
290+
}
291+
else:
292+
iface_msgs = all_iface_msgs
293+
294+
iface_keys = set(iface_msgs.keys())
295+
all_iface_keys = set(all_iface_msgs.keys())
296+
scicall_keys = set(scicall_wrappers.keys())
297+
298+
unwrapped = sorted(iface_keys - scicall_keys, key=lambda k: iface_msgs[k]["num"])
299+
# Stale check always uses the full iface (not filtered)
300+
stale = sorted(scicall_keys - all_iface_keys)
301+
wrapped = sorted(iface_keys & scicall_keys, key=lambda k: iface_msgs[k]["num"])
302+
303+
# Generate mode: output DeclareSciCall* lines grouped by category
304+
if args.generate:
305+
non_deprecated = [
306+
k for k in unwrapped if iface_msgs[k]["category"] != "Deprecated"
307+
]
308+
by_cat = {}
309+
for key in non_deprecated:
310+
cat = iface_msgs[key]["category"]
311+
by_cat.setdefault(cat, []).append(key)
312+
313+
for cat in sorted(by_cat.keys()):
314+
keys = by_cat[cat]
315+
print(f"// --- [{cat}] ({len(keys)} wrappers) ---")
316+
for key in keys:
317+
msg = iface_msgs[key]
318+
line = generate_wrapper(msg)
319+
print(line)
320+
print()
321+
print(f"// Total: {len(non_deprecated)} generated wrappers")
322+
return 0
323+
324+
# Group unwrapped by category
325+
unwrapped_by_cat = {}
326+
for key in unwrapped:
327+
cat = iface_msgs[key]["category"]
328+
unwrapped_by_cat.setdefault(cat, []).append(key)
329+
330+
# Summary
331+
cat_label = f" (category: {args.category})" if args.category else ""
332+
print(f"=== SciCall.h Coverage Report{cat_label} ===")
333+
print(f" iface messages (fun/get/set): {len(iface_msgs)}")
334+
print(f" SciCall.h wrappers: {len(scicall_wrappers)}")
335+
print(f" Wrapped (matched): {len(wrapped)}")
336+
print(f" Unwrapped (missing): {len(unwrapped)}")
337+
print(f" Stale/custom (no iface): {len(stale)}")
338+
print()
339+
340+
# Unwrapped messages
341+
if unwrapped:
342+
print(f"--- Unwrapped iface messages ({len(unwrapped)}) ---")
343+
for cat in sorted(unwrapped_by_cat.keys()):
344+
keys = unwrapped_by_cat[cat]
345+
print(f"\n [{cat}] ({len(keys)} messages)")
346+
for key in keys:
347+
msg = iface_msgs[key]
348+
if args.verbose:
349+
print(f" SCI_{key} = {msg['num']}")
350+
print(f" {msg['type']} {msg['ret']} {msg['name']}({msg['params']})")
351+
print(f" iface line {msg['line']}")
352+
else:
353+
print(f" SCI_{key:<45s} {msg['type']:<4s} {msg['ret']:<16s} {msg['name']}({msg['params']})")
354+
print()
355+
356+
# Stale wrappers (in SciCall.h but not in iface)
357+
if stale:
358+
print(f"--- Stale/custom wrappers ({len(stale)}) ---")
359+
print(" (In SciCall.h but no matching iface fun/get/set)")
360+
for key in stale:
361+
w = scicall_wrappers[key]
362+
status = " [commented]" if w["commented"] else ""
363+
print(f" SCI_{key:<45s} SciCall_{w['fn']:<30s} line {w['line']}{status}")
364+
print()
365+
366+
# Wrapped messages (optional)
367+
if args.show_wrapped and wrapped:
368+
print(f"--- Wrapped messages ({len(wrapped)}) ---")
369+
for key in wrapped:
370+
msg = iface_msgs[key]
371+
w = scicall_wrappers[key]
372+
status = " [commented]" if w["commented"] else ""
373+
print(f" SCI_{key:<45s} -> SciCall_{w['fn']}{status}")
374+
print()
375+
376+
# Exit code: 0 if no unwrapped non-deprecated messages, 1 otherwise
377+
non_deprecated_unwrapped = [
378+
k for k in unwrapped if iface_msgs[k]["category"] != "Deprecated"
379+
]
380+
if non_deprecated_unwrapped:
381+
print(f"({len(non_deprecated_unwrapped)} unwrapped non-deprecated messages)")
382+
return 0
383+
384+
385+
if __name__ == "__main__":
386+
sys.exit(main())

0 commit comments

Comments
 (0)