fivetech commited on
Commit
19b5f82
·
verified ·
1 Parent(s): ddde6c6

Upload generate_dataset.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. generate_dataset.py +1025 -0
generate_dataset.py ADDED
@@ -0,0 +1,1025 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Harbour PRG/CH Dataset Generator for Fine-tuning qwen2.5-coder:14b
4
+ Generates structured JSONL dataset from .prg and .ch files with descriptions.
5
+ CLEANED VERSION - removes boilerplate, ensures code quality.
6
+ """
7
+
8
+ import os
9
+ import re
10
+ import json
11
+ import random
12
+ from pathlib import Path
13
+ from typing import Dict, List, Tuple, Optional
14
+
15
+ # Configuration
16
+ HARBOUR_ROOT = Path("/home/fivetech/harbour")
17
+ OUTPUT_DIR = Path("/home/fivetech/finetune")
18
+ MAX_CODE_LENGTH = 8000
19
+ MIN_CODE_LENGTH = 80 # Minimum chars of actual code
20
+ TRAIN_RATIO = 0.9
21
+
22
+ # Module descriptions for contrib
23
+ MODULE_DESCRIPTIONS = {
24
+ "hbhttpd": "Multithreaded HTTP/HTTPS server framework",
25
+ "hbwin": "Windows API wrapper functions",
26
+ "hbpgsql": "PostgreSQL database client library",
27
+ "hbmysql": "MySQL database client library",
28
+ "hbsqlit3": "SQLite3 database client library",
29
+ "hbodbc": "ODBC database connectivity",
30
+ "hbtip": "Internet protocol utilities (FTP, HTTP, SMTP, POP3)",
31
+ "hbcurl": "libcurl wrapper for HTTP/FTP/SMTP operations",
32
+ "hbssl": "OpenSSL wrapper for SSL/TLS encryption",
33
+ "hbnf": "NanForum Toolkit - legacy Clipper compatibility functions",
34
+ "hbct": "CA-Tools compatibility library",
35
+ "hbmisc": "Miscellaneous utility functions",
36
+ "hbgd": "Graphics drawing library (GD)",
37
+ "hbcairo": "Cairo graphics library wrapper",
38
+ "hbhpdf": "PDF generation library (libharu)",
39
+ "hbbmp": "BMP image handling",
40
+ "hbzebra": "Barcode generation library",
41
+ "hbexpat": "XML parsing library (Expat)",
42
+ "hbmxml": "XML generation library",
43
+ "hbnetio": "Network I/O operations",
44
+ "hbpipeio": "Process pipe I/O operations",
45
+ "hbmemio": "Memory file I/O operations",
46
+ "xhb": "Extended Harbour functions",
47
+ "hbxpp": "xBase++ compatibility functions",
48
+ "hbunix": "Unix-specific functions",
49
+ "hbtpathy": "Telepath communication library",
50
+ "hbblat": "Blat email sending utility",
51
+ "hbblink": "Blinker function extender",
52
+ "hbgs": "Ghostscript wrapper",
53
+ "hbfship": "Fships library functions",
54
+ "hbmzip": "ZIP file handling",
55
+ "hbziparc": "ZIP archive handling",
56
+ "hbxdiff": "File difference/patching",
57
+ "hblzf": "LZF compression library",
58
+ "hbmlzo": "LZO compression library",
59
+ "hbbz2": "BZ2 compression library",
60
+ "hbformat": "Text formatting utilities",
61
+ "hbfoxpro": "FoxPro file format support",
62
+ "hbplist": "Apple plist file format support",
63
+ "hbcups": "CUPS printing system wrapper",
64
+ "hbsms": "SMS sending via modem",
65
+ "hbcomm": "Serial communication library",
66
+ "hbfbird": "Firebird database client",
67
+ "hbfimage": "FreeImage library wrapper",
68
+ "hbdoc": "Documentation generation utilities",
69
+ "hbtinymt": "Tiny Mersenne Twister PRNG",
70
+ "hbtest": "Test framework utilities",
71
+ }
72
+
73
+
74
+ def remove_all_comments(code: str) -> str:
75
+ """Remove ALL comments from Harbour code: //, /* */, *, and inline comments."""
76
+ # Remove block comments /* ... */ (including multi-line)
77
+ code = re.sub(r'/\*.*?\*/', '', code, flags=re.DOTALL)
78
+
79
+ # Remove single-line comments // ...
80
+ code = re.sub(r'//[^\n]*', '', code)
81
+
82
+ # Remove lines that are only star-prefixed comments: * ...
83
+ # And standalone star lines used in block comment formatting
84
+ lines = code.split("\n")
85
+ result_lines = []
86
+ for line in lines:
87
+ stripped = line.strip()
88
+ # Skip lines that are ONLY comments (star-prefixed, standalone)
89
+ if re.match(r'^\*\s', stripped) or stripped == '*' or stripped == '*/' or stripped == '/*':
90
+ continue
91
+ result_lines.append(line)
92
+ code = "\n".join(result_lines)
93
+
94
+ # Remove inline comments at end of lines: code * comment
95
+ # Pattern: something followed by whitespace then * ... (but not in strings)
96
+ code = re.sub(r'([^\s"].*?)\s+\*[^"]*$', r'\1', code, flags=re.MULTILINE)
97
+
98
+ return code
99
+
100
+
101
+ def clean_excessive_blank_lines(code: str) -> str:
102
+ """Remove excessive blank lines (more than 2 consecutive)."""
103
+ lines = code.split("\n")
104
+ result_lines = []
105
+ blank_count = 0
106
+
107
+ for line in lines:
108
+ if line.strip() == "":
109
+ blank_count += 1
110
+ if blank_count <= 2:
111
+ result_lines.append(line)
112
+ else:
113
+ blank_count = 0
114
+ result_lines.append(line)
115
+
116
+ return "\n".join(result_lines)
117
+
118
+
119
+ def remove_disabled_code(code: str) -> str:
120
+ """Remove #if 0 ... #endif blocks (disabled code)."""
121
+ lines = code.split("\n")
122
+ result_lines = []
123
+ in_disabled = False
124
+ disabled_depth = 0
125
+
126
+ for line in lines:
127
+ stripped = line.strip()
128
+
129
+ if stripped.upper().startswith("#IF 0") or stripped.upper().startswith("#IFDEF _HARBOUR_DISABLE"):
130
+ in_disabled = True
131
+ disabled_depth += 1
132
+ continue
133
+
134
+ if in_disabled:
135
+ if stripped.upper().startswith("#ENDIF"):
136
+ disabled_depth -= 1
137
+ if disabled_depth <= 0:
138
+ in_disabled = False
139
+ continue
140
+
141
+ result_lines.append(line)
142
+
143
+ return "\n".join(result_lines)
144
+
145
+
146
+ def is_ch_file(filepath: Path) -> bool:
147
+ """Check if file is a .ch (Clipper header) file."""
148
+ return filepath.suffix.lower() == '.ch'
149
+
150
+
151
+ def has_real_ch_code(code: str) -> bool:
152
+ """Check if .ch file contains actual preprocessor definitions."""
153
+ upper = code.upper()
154
+
155
+ # Must have at least one preprocessor construct
156
+ ch_constructs = [
157
+ "#XCOMMAND", "#XTRANSLATE", "#COMMAND", "#TRANSLATE",
158
+ "#DEFINE", "#UNDEF", "#IFDEF", "#IFNDEF", "#IF ",
159
+ "#INCLUDE", "#PRAGMA", "#ENDPROC",
160
+ ]
161
+ if not any(construct in upper for construct in ch_constructs):
162
+ return False
163
+
164
+ # Count actual definition lines
165
+ lines = code.split("\n")
166
+ def_lines = 0
167
+ for line in lines:
168
+ stripped = line.strip()
169
+ if not stripped:
170
+ continue
171
+ if stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
172
+ continue
173
+ if stripped.startswith("#"):
174
+ def_lines += 1
175
+
176
+ return def_lines >= 3
177
+
178
+
179
+ def extract_ch_definitions(code: str) -> Dict:
180
+ """Extract definitions from .ch file."""
181
+ defines = []
182
+ commands = []
183
+ translates = []
184
+
185
+ for line in code.split("\n"):
186
+ stripped = line.strip()
187
+ upper = stripped.upper()
188
+
189
+ if upper.startswith("#DEFINE ") or upper.startswith("#UNDEF "):
190
+ parts = stripped.split()
191
+ if len(parts) >= 2:
192
+ defines.append(parts[1])
193
+ elif upper.startswith("#XCOMMAND") or upper.startswith("#COMMAND"):
194
+ commands.append(stripped[:60])
195
+ elif upper.startswith("#XTRANSLATE") or upper.startswith("#TRANSLATE"):
196
+ translates.append(stripped[:60])
197
+
198
+ return {
199
+ "defines": defines[:10],
200
+ "commands": commands[:5],
201
+ "translates": translates[:5]
202
+ }
203
+
204
+
205
+ def generate_ch_description(filepath: Path, code: str) -> str:
206
+ """Generate description for .ch file."""
207
+ rel_path = filepath.relative_to(HARBOUR_ROOT)
208
+ filename = filepath.stem
209
+
210
+ desc_parts = []
211
+
212
+ # Determine location context
213
+ if rel_path.parts[0] == "include":
214
+ desc_parts.append(f"Harbour header file: {rel_path}")
215
+ elif rel_path.parts[0] == "contrib":
216
+ module = rel_path.parts[1] if len(rel_path.parts) > 1 else ""
217
+ module_desc = MODULE_DESCRIPTIONS.get(module, "")
218
+ if module_desc:
219
+ desc_parts.append(f"Header file for contribution module '{module}' ({module_desc}): {rel_path}")
220
+ else:
221
+ desc_parts.append(f"Header file for contribution module '{module}': {rel_path}")
222
+ else:
223
+ desc_parts.append(f"Harbour header file: {rel_path}")
224
+
225
+ # Extract and describe definitions
226
+ defs = extract_ch_definitions(code)
227
+
228
+ if defs["defines"]:
229
+ if len(defs["defines"]) <= 5:
230
+ desc_parts.append(f"Defines constants: {', '.join(defs['defines'])}")
231
+ else:
232
+ desc_parts.append(f"Defines {len(defs['defines'])} constants including: {', '.join(defs['defines'][:5])}")
233
+
234
+ if defs["commands"]:
235
+ desc_parts.append(f"Declares {len(defs['commands'])} preprocessor commands")
236
+
237
+ if defs["translates"]:
238
+ desc_parts.append(f"Declares {len(defs['translates'])} preprocessor translations")
239
+
240
+ # Common header purposes
241
+ upper = code.upper()
242
+ if "ES_" in upper or "EG_" in upper or "ERROR" in filename.upper():
243
+ desc_parts.append("Defines error handling constants and codes")
244
+ elif "INKEY" in upper or "K_" in upper:
245
+ desc_parts.append("Defines keyboard input constants")
246
+ elif "SET" in upper and "CH" in filename.upper():
247
+ desc_parts.append("Defines SET command options")
248
+ elif "COLOR" in upper or "_SET_" in upper:
249
+ desc_parts.append("Defines color and display constants")
250
+ elif "HB_" in upper or "HBEXT" in upper:
251
+ desc_parts.append("Defines Harbour internal constants and macros")
252
+ elif "THREAD" in upper:
253
+ desc_parts.append("Defines threading constants and macros")
254
+ elif "FILE" in upper or "F_" in upper:
255
+ desc_parts.append("Defines file I/O constants")
256
+ elif "DB" in upper or "RDD" in upper:
257
+ desc_parts.append("Defines database/RDD constants")
258
+ elif "COM" in upper or "SERIAL" in upper:
259
+ desc_parts.append("Defines communication constants")
260
+ elif "GT" in upper:
261
+ desc_parts.append("Defines graphics terminal constants")
262
+ elif "BOX" in upper or "BORDER" in upper:
263
+ desc_parts.append("Defines box and border drawing constants")
264
+ elif "MEMO" in upper:
265
+ desc_parts.append("Defines memo field constants")
266
+
267
+ return ". ".join(desc_parts)
268
+
269
+
270
+ def has_real_code(code: str) -> bool:
271
+ """Check if code contains actual Harbour code (not just comments/includes)."""
272
+ upper = code.upper()
273
+
274
+ # Must have at least one of these code constructs
275
+ code_constructs = [
276
+ "FUNCTION ", "PROCEDURE ", "CREATE CLASS", "ENDCLASS",
277
+ "METHOD ", "RETURN ", "LOCAL ", "MEMVAR ",
278
+ "THREAD STATIC", "IF ", "FOR ", "WHILE ", "DO CASE",
279
+ "BEGIN SEQUENCE", "SWITCH ", "REQUEST ",
280
+ "INIT PROCEDURE", "EXIT PROCEDURE",
281
+ ]
282
+ if not any(construct in upper for construct in code_constructs):
283
+ return False
284
+
285
+ # Count actual code lines (non-empty, non-preprocessor, non-blank)
286
+ lines = code.split("\n")
287
+ code_lines = 0
288
+ for line in lines:
289
+ stripped = line.strip()
290
+ if not stripped:
291
+ continue
292
+ if stripped.startswith("#"):
293
+ continue
294
+ if stripped.startswith("//") or stripped.startswith("/*") or stripped.startswith("*"):
295
+ continue
296
+ code_lines += 1
297
+
298
+ return code_lines >= 8
299
+
300
+
301
+ def is_code_complete(code: str) -> bool:
302
+ """Check if code is complete (proper ENDCLASS, balanced structures)."""
303
+ upper = code.upper()
304
+
305
+ # Check class definitions have matching ENDCLASS
306
+ class_count = upper.count("CREATE CLASS")
307
+ endclass_count = upper.count("ENDCLASS")
308
+ if class_count > 0 and endclass_count < class_count:
309
+ return False
310
+
311
+ # Check DO CASE has ENDDO CASE
312
+ docase_count = upper.count("DO CASE")
313
+ endcase_count = upper.count("ENDCASE") + upper.count("END CASE")
314
+ if docase_count > 0 and endcase_count < docase_count:
315
+ return False
316
+
317
+ # Check FOR/NEXT balance
318
+ for_count = len(re.findall(r'\bFOR\s+\w+', upper))
319
+ next_count = upper.count("\nNEXT") + (1 if upper.endswith("NEXT") else 0)
320
+ # Be lenient - some code uses EXIT in loops
321
+
322
+ # Check DO WHILE / ENDDO balance
323
+ dowhile_count = upper.count("DO WHILE")
324
+ enddo_count = upper.count("ENDDO") + upper.count("END DO")
325
+ if dowhile_count > 0 and enddo_count < dowhile_count:
326
+ return False
327
+
328
+ # Check BEGIN SEQUENCE / END / RECOVER balance
329
+ seq_count = upper.count("BEGIN SEQUENCE")
330
+ end_count = upper.count("\nEND\n") + upper.count("\nEND ") + (1 if upper.endswith("\nEND") or upper.endswith(" END") else 0)
331
+
332
+ # Check parentheses balance (lenient)
333
+ open_p = code.count('(')
334
+ close_p = code.count(')')
335
+ if abs(open_p - close_p) > 3:
336
+ return False
337
+
338
+ # Check BEGIN/END blocks
339
+ begin_count = len(re.findall(r'\bBEGIN\b', upper))
340
+ end_block_count = len(re.findall(r'\bEND\b', upper)) - upper.count("ENDCLASS") - upper.count("ENDCASE") - upper.count("END IF") - upper.count("ENDDO")
341
+ # Very lenient check - just ensure it's not wildly unbalanced
342
+ if begin_count > 0 and end_block_count > begin_count + 5:
343
+ return False
344
+
345
+ return True
346
+
347
+
348
+ def extract_classes_and_functions(code: str) -> Dict:
349
+ """Extract class and function definitions from code."""
350
+ classes = []
351
+ functions = []
352
+ procedures = []
353
+
354
+ for line in code.split("\n"):
355
+ line_stripped = line.strip()
356
+ upper = line_stripped.upper()
357
+
358
+ # Class definitions
359
+ if upper.startswith("CREATE CLASS"):
360
+ parts = line_stripped.split()
361
+ if len(parts) >= 3:
362
+ class_name = parts[2]
363
+ classes.append(class_name)
364
+
365
+ # Function definitions
366
+ if upper.startswith("FUNCTION ") or (upper.startswith("STATIC FUNCTION ")):
367
+ parts = line_stripped.split()
368
+ idx = 2 if upper.startswith("STATIC") else 1
369
+ if len(parts) >= idx + 1:
370
+ func_name = parts[idx].split("(")[0]
371
+ functions.append(func_name)
372
+
373
+ # Procedure definitions
374
+ if upper.startswith("PROCEDURE ") or upper.startswith("STATIC PROCEDURE "):
375
+ parts = line_stripped.split()
376
+ idx = 2 if upper.startswith("STATIC") else 1
377
+ if len(parts) >= idx + 1:
378
+ proc_name = parts[idx].split("(")[0]
379
+ procedures.append(proc_name)
380
+
381
+ # INIT/EXIT procedures
382
+ if upper.startswith("INIT PROCEDURE") or upper.startswith("EXIT PROCEDURE"):
383
+ parts = line_stripped.split()
384
+ if len(parts) >= 3:
385
+ proc_name = parts[2].split("(")[0]
386
+ procedures.append(proc_name)
387
+
388
+ return {
389
+ "classes": classes,
390
+ "functions": functions,
391
+ "procedures": procedures
392
+ }
393
+
394
+
395
+ def categorize_file(filepath: Path) -> Tuple[str, str]:
396
+ """Categorize a PRG file into category and subcategory."""
397
+ rel_path = filepath.relative_to(HARBOUR_ROOT)
398
+ parts = rel_path.parts
399
+
400
+ if parts[0] == "src":
401
+ if parts[1] == "rtl":
402
+ return "rtl", categorize_rtl_file(filepath)
403
+ elif parts[1] == "rdd":
404
+ return "rdd", "rdd_core"
405
+ elif parts[1] == "debug":
406
+ return "rtl", "utility"
407
+ else:
408
+ return "rtl", "utility"
409
+ elif parts[0] == "contrib":
410
+ module = parts[1] if len(parts) > 1 else "unknown"
411
+ return "contrib", categorize_contrib_module(module)
412
+ elif parts[0] == "tests":
413
+ return "tests", categorize_test_file(filepath)
414
+ elif parts[0] == "utils":
415
+ return "utils", categorize_utils_file(filepath)
416
+ elif parts[0] == "extras":
417
+ return "extras", categorize_extras_file(filepath)
418
+ else:
419
+ return "rtl", "utility"
420
+
421
+
422
+ def categorize_rtl_file(filepath: Path) -> str:
423
+ """Categorize RTL files into subcategories."""
424
+ name = filepath.stem.lower()
425
+
426
+ if name.startswith("t") and not name.startswith("text"):
427
+ if any(x in name for x in ["get", "browse", "column", "editor", "scalar", "object", "class"]):
428
+ return "oop_class"
429
+ elif any(x in name for x in ["menu", "popup", "topbar"]):
430
+ return "ui_menu"
431
+ elif any(x in name for x in ["check", "radio", "push", "list", "label", "button"]):
432
+ return "ui_widget"
433
+ elif any(x in name for x in ["edit", "memo"]):
434
+ return "text_edit"
435
+ elif any(x in name for x in ["persist", "profile", "symbol"]):
436
+ return "oop_class"
437
+ else:
438
+ return "oop_class"
439
+ elif "get" in name or "read" in name:
440
+ return "get_system"
441
+ elif any(x in name for x in ["err", "alert"]):
442
+ return "error_handling"
443
+ elif any(x in name for x in ["file", "dir", "ini", "type"]):
444
+ return "file_io"
445
+ elif any(x in name for x in ["db", "memo"]):
446
+ return "database"
447
+ else:
448
+ return "utility"
449
+
450
+
451
+ def categorize_contrib_module(module: str) -> str:
452
+ """Categorize contrib modules."""
453
+ db_modules = {"hbpgsql", "hbmysql", "hbsqlit3", "hbodbc", "hbfbird", "rddsql", "rddpg",
454
+ "rddmy", "rddfb", "rddads", "rddbm", "rddmisc", "sddpg", "sddmy",
455
+ "sddoci", "sddodbc", "sddsqlt3", "sddfb", "rddado"}
456
+ net_modules = {"hbtip", "hbcurl", "hbhttpd", "hbnetio", "hbcomio", "hbtcpio", "hbpipeio"}
457
+ sec_modules = {"hbssl", "hbmagic"}
458
+ gfx_modules = {"hbbmp", "hbcairo", "hbhpdf", "hbgd", "hbzebra", "hbfimage", "hbformat"}
459
+ fmt_modules = {"hbexpat", "hbmxml", "hbfoxpro", "hbplist", "hbmemio"}
460
+ plat_modules = {"hbwin", "hbunix", "hboslib", "gtalleg", "gtwvg", "gtwvw", "gtwvb"}
461
+ compat_modules = {"hbnf", "hbct", "xhb", "hbxpp", "hbtpathy", "hbfship"}
462
+
463
+ if module in db_modules:
464
+ return "database"
465
+ elif module in net_modules:
466
+ return "network"
467
+ elif module in sec_modules:
468
+ return "security"
469
+ elif module in gfx_modules:
470
+ return "graphics"
471
+ elif module in fmt_modules:
472
+ return "data_format"
473
+ elif module in plat_modules:
474
+ return "platform"
475
+ elif module in compat_modules:
476
+ return "compatibility"
477
+ else:
478
+ return "utility"
479
+
480
+
481
+ def categorize_test_file(filepath: Path) -> str:
482
+ """Categorize test files."""
483
+ name = filepath.stem.lower()
484
+
485
+ if any(x in name for x in ["class", "oob", "inherit", "scope", "data"]):
486
+ return "oop"
487
+ elif any(x in name for x in ["db", "rdd", "browse"]):
488
+ return "database"
489
+ elif any(x in name for x in ["speed", "bench"]):
490
+ return "performance"
491
+ elif any(x in name for x in ["str", "math", "date", "array", "for", "while", "if", "case",
492
+ "static", "mem", "gt", "regex", "file", "err", "hello"]):
493
+ return "language_basics"
494
+ else:
495
+ return "function_api"
496
+
497
+
498
+ def categorize_utils_file(filepath: Path) -> str:
499
+ """Categorize utility files."""
500
+ name = filepath.stem.lower()
501
+
502
+ if "hbmk" in name or "build" in name:
503
+ return "build_system"
504
+ elif "test" in name or "rt_" in name:
505
+ return "test_framework"
506
+ elif "i18n" in name or "lang" in name:
507
+ return "i18n"
508
+ else:
509
+ return "build_system"
510
+
511
+
512
+ def categorize_extras_file(filepath: Path) -> str:
513
+ """Categorize extras files."""
514
+ parts = filepath.relative_to(HARBOUR_ROOT).parts
515
+
516
+ if len(parts) > 1:
517
+ module = parts[1].lower()
518
+ if "pdf" in module or "vpdf" in module:
519
+ return "pdf"
520
+ elif "xls" in module or "excel" in module:
521
+ return "spreadsheet"
522
+ elif "srv" in module or "http" in module:
523
+ return "server"
524
+ return "utility"
525
+
526
+
527
+ def generate_description(filepath: Path, code: str, category: str, subcategory: str) -> str:
528
+ """Generate a comprehensive description for a PRG file."""
529
+ rel_path = filepath.relative_to(HARBOUR_ROOT)
530
+ module_name = rel_path.parts[1] if len(rel_path.parts) > 1 else "rtl"
531
+
532
+ # Get module description if contrib
533
+ module_desc = ""
534
+ if category == "contrib" and module_name in MODULE_DESCRIPTIONS:
535
+ module_desc = MODULE_DESCRIPTIONS[module_name]
536
+
537
+ # Extract code elements
538
+ elements = extract_classes_and_functions(code)
539
+
540
+ # Generate description based on category and content
541
+ desc_parts = []
542
+
543
+ # File location context
544
+ if category == "rtl":
545
+ desc_parts.append(f"Harbour Runtime Library file: {rel_path}")
546
+ elif category == "contrib":
547
+ desc_parts.append(f"Harbour contribution module '{module_name}' ({module_desc}): {rel_path}")
548
+ elif category == "tests":
549
+ desc_parts.append(f"Harbour test program: {rel_path}")
550
+ elif category == "utils":
551
+ desc_parts.append(f"Harbour utility program: {rel_path}")
552
+ elif category == "extras":
553
+ desc_parts.append(f"Harbour extra library: {rel_path}")
554
+ else:
555
+ desc_parts.append(f"Harbour source file: {rel_path}")
556
+
557
+ # Add code structure information
558
+ if elements["classes"]:
559
+ desc_parts.append(f"Defines classes: {', '.join(elements['classes'][:5])}")
560
+
561
+ if elements["functions"]:
562
+ if len(elements["functions"]) <= 5:
563
+ desc_parts.append(f"Provides functions: {', '.join(elements['functions'])}")
564
+ else:
565
+ desc_parts.append(f"Provides {len(elements['functions'])} functions including: {', '.join(elements['functions'][:5])}")
566
+
567
+ if elements["procedures"]:
568
+ if len(elements["procedures"]) <= 3:
569
+ desc_parts.append(f"Contains procedures: {', '.join(elements['procedures'])}")
570
+ else:
571
+ desc_parts.append(f"Contains {len(elements['procedures'])} procedures")
572
+
573
+ # Add subcategory context
574
+ subcategory_descriptions = {
575
+ "oop_class": "This file implements object-oriented classes using Harbour's class system",
576
+ "ui_widget": "This file defines UI widget classes for graphical interfaces",
577
+ "ui_menu": "This file implements menu system classes",
578
+ "text_edit": "This file provides text editing functionality",
579
+ "get_system": "This file implements the GET system for input field handling",
580
+ "scalar_type": "This file defines scalar type wrapper classes",
581
+ "error_handling": "This file implements error handling and reporting",
582
+ "file_io": "This file provides file I/O operations",
583
+ "database": "This file handles database operations",
584
+ "utility": "This file provides utility functions",
585
+ "rdd_core": "This file implements core Record Driver Driver functionality",
586
+ "rdd_driver": "This file implements a database driver",
587
+ "network": "This file provides network protocol implementations",
588
+ "security": "This file implements security and encryption functions",
589
+ "graphics": "This file provides graphics and image processing capabilities",
590
+ "data_format": "This file handles data format parsing and generation",
591
+ "platform": "This file provides platform-specific functionality",
592
+ "compatibility": "This file provides legacy compatibility functions",
593
+ "language_basics": "This test file exercises basic Harbour language features",
594
+ "function_api": "This test file tests specific function APIs",
595
+ "oop": "This test file tests object-oriented programming features",
596
+ "performance": "This test file benchmarks performance characteristics",
597
+ "build_system": "This file is part of the build system tooling",
598
+ "test_framework": "This file is part of the test framework",
599
+ "i18n": "This file provides internationalization support",
600
+ "pdf": "This file provides PDF generation capabilities",
601
+ "spreadsheet": "This file provides spreadsheet generation capabilities",
602
+ "server": "This file implements server functionality",
603
+ }
604
+
605
+ if subcategory in subcategory_descriptions:
606
+ desc_parts.append(subcategory_descriptions[subcategory])
607
+
608
+ return ". ".join(desc_parts)
609
+
610
+
611
+ def create_training_entry(filepath: Path, code: str, description: str) -> Dict:
612
+ """Create a training entry in the instruction format."""
613
+ return {
614
+ "instruction": f"Write Harbour (xBase/Clipper) code for: {description}",
615
+ "input": "",
616
+ "output": code,
617
+ "metadata": {
618
+ "file_path": str(filepath.relative_to(HARBOUR_ROOT)),
619
+ "language": "harbour",
620
+ "description": description
621
+ }
622
+ }
623
+
624
+
625
+ def create_completion_entry(filepath: Path, code: str, description: str) -> Dict:
626
+ """Create a completion-style training entry with diverse instructions."""
627
+ # Generate diverse user prompts based on code content
628
+ import random
629
+ random.seed(hash(filepath)) # Deterministic per file
630
+
631
+ elements = extract_classes_and_functions(code)
632
+ upper = code.upper()
633
+
634
+ # Different prompt templates based on content
635
+ templates = []
636
+
637
+ if elements["classes"]:
638
+ templates.append(f"Implement the following Harbour classes: {', '.join(elements['classes'][:3])}. {description}")
639
+ templates.append(f"Create Harbour OOP classes for the functionality described: {description}")
640
+
641
+ if elements["functions"]:
642
+ templates.append(f"Write Harbour functions: {', '.join(elements['functions'][:3])}. {description}")
643
+ templates.append(f"Implement these Harbour functions: {description}")
644
+
645
+ if elements["procedures"]:
646
+ templates.append(f"Write a Harbour program with procedures: {', '.join(elements['procedures'][:3])}. {description}")
647
+
648
+ if "#DEFINE" in upper or "#XCOMMAND" in upper:
649
+ templates.append(f"Create Harbour preprocessor definitions: {description}")
650
+ templates.append(f"Define Harbour macros and constants: {description}")
651
+
652
+ # General templates
653
+ templates.append(f"Write the following Harbour (xBase/Clipper) code:\n\n{description}")
654
+ templates.append(f"Implement this Harbour module: {description}")
655
+ templates.append(f"Here is a Harbour (xBase/Clipper) implementation:\n\n{description}")
656
+ templates.append(f"Generate Harbour code for: {description}")
657
+
658
+ # Select a random template
659
+ user_prompt = random.choice(templates)
660
+
661
+ return {
662
+ "messages": [
663
+ {
664
+ "role": "system",
665
+ "content": "You are an expert Harbour (xBase/Clipper) programmer. Write clean, efficient code following Harbour conventions. Use proper Hungarian notation for variable names (c=character, n=numeric, l=logical, a=array, o=object, b=codeblock)."
666
+ },
667
+ {
668
+ "role": "user",
669
+ "content": user_prompt
670
+ },
671
+ {
672
+ "role": "assistant",
673
+ "content": code
674
+ }
675
+ ],
676
+ "metadata": {
677
+ "file_path": str(filepath.relative_to(HARBOUR_ROOT)),
678
+ "language": "harbour",
679
+ "description": description
680
+ }
681
+ }
682
+
683
+
684
+ def process_prg_file(filepath: Path) -> List[Dict]:
685
+ """Process a single PRG file and generate training entries."""
686
+ try:
687
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
688
+ code = f.read()
689
+ except Exception as e:
690
+ print(f"Error reading {filepath}: {e}")
691
+ return []
692
+
693
+ # Skip empty files or very small files
694
+ if len(code.strip()) < 50:
695
+ return []
696
+
697
+ # Step 1: Remove ALL comments (block, single-line, star-prefixed, inline)
698
+ code = remove_all_comments(code)
699
+
700
+ # Step 2: Remove disabled code blocks (#if 0)
701
+ code = remove_disabled_code(code)
702
+
703
+ # Step 3: Clean excessive blank lines
704
+ code = clean_excessive_blank_lines(code)
705
+
706
+ # Step 5: Strip leading/trailing whitespace
707
+ code = code.strip()
708
+
709
+ # Skip if no real code remains
710
+ if not has_real_code(code):
711
+ return []
712
+
713
+ # Skip if code is too short
714
+ if len(code) < MIN_CODE_LENGTH:
715
+ return []
716
+
717
+ # Skip if code is incomplete
718
+ if not is_code_complete(code):
719
+ return []
720
+
721
+ # Truncate if too long
722
+ if len(code) > MAX_CODE_LENGTH:
723
+ lines = code.split("\n")
724
+ truncated_lines = []
725
+ current_length = 0
726
+ for line in lines:
727
+ if current_length + len(line) > MAX_CODE_LENGTH:
728
+ break
729
+ truncated_lines.append(line)
730
+ current_length += len(line) + 1
731
+ code = "\n".join(truncated_lines)
732
+
733
+ # Categorize the file
734
+ category, subcategory = categorize_file(filepath)
735
+
736
+ # Generate description
737
+ description = generate_description(filepath, code, category, subcategory)
738
+
739
+ # Create training entry (chat format only - preferred for Qwen2.5-Coder)
740
+ entry = create_completion_entry(filepath, code, description)
741
+ entry["metadata"]["category"] = category
742
+ entry["metadata"]["subcategory"] = subcategory
743
+
744
+ return [entry]
745
+
746
+
747
+ def process_ch_file(filepath: Path) -> List[Dict]:
748
+ """Process a single CH file and generate training entries."""
749
+ try:
750
+ with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
751
+ code = f.read()
752
+ except Exception as e:
753
+ print(f"Error reading {filepath}: {e}")
754
+ return []
755
+
756
+ # Skip empty files
757
+ if len(code.strip()) < 30:
758
+ return []
759
+
760
+ # Step 1: Remove ALL comments
761
+ code = remove_all_comments(code)
762
+
763
+ # Step 2: Remove disabled code blocks (#if 0)
764
+ code = remove_disabled_code(code)
765
+
766
+ # Step 3: Clean excessive blank lines
767
+ code = clean_excessive_blank_lines(code)
768
+
769
+ # Step 4: Strip leading/trailing whitespace
770
+ code = code.strip()
771
+
772
+ # Skip if no real code remains
773
+ if not has_real_ch_code(code):
774
+ return []
775
+
776
+ # Skip if code is too short
777
+ if len(code) < MIN_CODE_LENGTH:
778
+ return []
779
+
780
+ # Truncate if too long
781
+ if len(code) > MAX_CODE_LENGTH:
782
+ lines = code.split("\n")
783
+ truncated_lines = []
784
+ current_length = 0
785
+ for line in lines:
786
+ if current_length + len(line) > MAX_CODE_LENGTH:
787
+ break
788
+ truncated_lines.append(line)
789
+ current_length += len(line) + 1
790
+ code = "\n".join(truncated_lines)
791
+
792
+ # Generate description
793
+ description = generate_ch_description(filepath, code)
794
+
795
+ # Determine category
796
+ rel_path = filepath.relative_to(HARBOUR_ROOT)
797
+ parts = rel_path.parts
798
+
799
+ if parts[0] == "include":
800
+ category = "include"
801
+ elif parts[0] == "contrib":
802
+ category = "contrib"
803
+ elif parts[0] == "utils":
804
+ category = "utils"
805
+ elif parts[0] == "extras":
806
+ category = "extras"
807
+ else:
808
+ category = "include"
809
+
810
+ # Create training entry (chat format only)
811
+ entry = create_completion_entry(filepath, code, description)
812
+ entry["metadata"]["category"] = category
813
+ entry["metadata"]["subcategory"] = "header"
814
+
815
+ return [entry]
816
+
817
+
818
+ def main():
819
+ """Main function to generate the dataset."""
820
+ print("=" * 60)
821
+ print("Harbour PRG/CH Dataset Generator (CLEANED)")
822
+ print("=" * 60)
823
+
824
+ # Find all PRG and CH files
825
+ print("\n1. Finding all Harbour source files...")
826
+ prg_files = list(HARBOUR_ROOT.rglob("*.prg"))
827
+ ch_files = list(HARBOUR_ROOT.rglob("*.ch"))
828
+ print(f" Found {len(prg_files)} PRG files")
829
+ print(f" Found {len(ch_files)} CH files")
830
+
831
+ # Process PRG files
832
+ print("\n2. Processing PRG files...")
833
+ all_entries = []
834
+ category_counts = {}
835
+ skipped_files = 0
836
+
837
+ for i, filepath in enumerate(prg_files, 1):
838
+ if i % 100 == 0:
839
+ print(f" Processing PRG file {i}/{len(prg_files)}...")
840
+
841
+ entries = process_prg_file(filepath)
842
+ if entries:
843
+ all_entries.extend(entries)
844
+ category = entries[0]["metadata"]["category"]
845
+ category_counts[category] = category_counts.get(category, 0) + 1
846
+ else:
847
+ skipped_files += 1
848
+
849
+ print(f"\n PRG: Generated {len(all_entries)} entries, skipped {skipped_files} files")
850
+
851
+ # Process CH files
852
+ print("\n3. Processing CH files...")
853
+ ch_entries = 0
854
+ ch_skipped = 0
855
+
856
+ for i, filepath in enumerate(ch_files, 1):
857
+ if i % 20 == 0:
858
+ print(f" Processing CH file {i}/{len(ch_files)}...")
859
+
860
+ entries = process_ch_file(filepath)
861
+ if entries:
862
+ all_entries.extend(entries)
863
+ ch_entries += 1
864
+ category = entries[0]["metadata"]["category"]
865
+ category_counts[category] = category_counts.get(category, 0) + 1
866
+ else:
867
+ ch_skipped += 1
868
+
869
+ print(f"\n CH: Generated {ch_entries} file entries, skipped {ch_skipped} files")
870
+ print(f"\n Total: {len(all_entries)} training entries")
871
+
872
+ # Print category statistics
873
+ print("\n3. Category statistics:")
874
+ for category, count in sorted(category_counts.items()):
875
+ print(f" {category}: {count} files")
876
+
877
+ # Shuffle entries
878
+ random.seed(42)
879
+ random.shuffle(all_entries)
880
+
881
+ # Split into train and validation
882
+ print("\n4. Splitting into train/validation sets...")
883
+ split_idx = int(len(all_entries) * TRAIN_RATIO)
884
+ train_entries = all_entries[:split_idx]
885
+ val_entries = all_entries[split_idx:]
886
+
887
+ print(f" Training set: {len(train_entries)} entries")
888
+ print(f" Validation set: {len(val_entries)} entries")
889
+
890
+ # Save datasets
891
+ print("\n5. Saving datasets...")
892
+
893
+ # Save as JSONL (instruction format)
894
+ train_jsonl_path = OUTPUT_DIR / "harbour_train.jsonl"
895
+ val_jsonl_path = OUTPUT_DIR / "harbour_val.jsonl"
896
+
897
+ with open(train_jsonl_path, 'w', encoding='utf-8') as f:
898
+ for entry in train_entries:
899
+ train_entry = {k: v for k, v in entry.items() if k != "metadata"}
900
+ f.write(json.dumps(train_entry, ensure_ascii=False) + "\n")
901
+
902
+ with open(val_jsonl_path, 'w', encoding='utf-8') as f:
903
+ for entry in val_entries:
904
+ val_entry = {k: v for k, v in entry.items() if k != "metadata"}
905
+ f.write(json.dumps(val_entry, ensure_ascii=False) + "\n")
906
+
907
+ print(f" Saved training JSONL: {train_jsonl_path}")
908
+ print(f" Saved validation JSONL: {val_jsonl_path}")
909
+
910
+ # Save full dataset with metadata
911
+ full_dataset_path = OUTPUT_DIR / "harbour_dataset_full.jsonl"
912
+ with open(full_dataset_path, 'w', encoding='utf-8') as f:
913
+ for entry in all_entries:
914
+ f.write(json.dumps(entry, ensure_ascii=False) + "\n")
915
+
916
+ print(f" Saved full dataset with metadata: {full_dataset_path}")
917
+
918
+ # Generate statistics file
919
+ total_files = len(prg_files) + len(ch_files)
920
+ stats = {
921
+ "total_prg_files": len(prg_files),
922
+ "total_ch_files": len(ch_files),
923
+ "total_files": total_files,
924
+ "total_entries": len(all_entries),
925
+ "skipped_prg": skipped_files,
926
+ "skipped_ch": ch_skipped,
927
+ "train_entries": len(train_entries),
928
+ "val_entries": len(val_entries),
929
+ "categories": category_counts,
930
+ "files_per_category": {}
931
+ }
932
+
933
+ for entry in all_entries:
934
+ cat = entry["metadata"]["category"]
935
+ subcat = entry["metadata"]["subcategory"]
936
+ if cat not in stats["files_per_category"]:
937
+ stats["files_per_category"][cat] = {}
938
+ stats["files_per_category"][cat][subcat] = stats["files_per_category"][cat].get(subcat, 0) + 1
939
+
940
+ stats_path = OUTPUT_DIR / "dataset_stats.json"
941
+ with open(stats_path, 'w', encoding='utf-8') as f:
942
+ json.dump(stats, f, indent=2, ensure_ascii=False)
943
+
944
+ print(f" Saved statistics: {stats_path}")
945
+
946
+ # Generate README
947
+ readme_content = f"""# Harbour Fine-tuning Dataset
948
+
949
+ ## Overview
950
+ This dataset contains {len(all_entries)} training entries extracted from:
951
+ - {len(prg_files)} Harbour PRG (.prg) source files
952
+ - {len(ch_files)} Harbour Header (.ch) files
953
+
954
+ {skipped_files} PRG files and {ch_skipped} CH files were skipped due to quality issues.
955
+
956
+ ## Dataset Format
957
+ The dataset is provided in JSONL format with the following structure:
958
+
959
+ ### Instruction Format (harbour_train.jsonl / harbour_val.jsonl)
960
+ ```json
961
+ {{"instruction": "...", "input": "", "output": "..."}}
962
+ ```
963
+
964
+ ### Full Dataset (harbour_dataset_full.jsonl)
965
+ ```json
966
+ {{"instruction": "...", "input": "", "output": "...", "metadata": {{"file_path": "...", "language": "harbour", "category": "...", "subcategory": "..."}}}}
967
+ ```
968
+
969
+ ## Categories
970
+ - **include**: Header files with constants/macros ({category_counts.get('include', 0)} files)
971
+ - **rtl**: Harbour Runtime Library ({category_counts.get('rtl', 0)} files)
972
+ - **contrib**: Contribution libraries ({category_counts.get('contrib', 0)} files)
973
+ - **tests**: Test programs ({category_counts.get('tests', 0)} files)
974
+ - **utils**: Utility programs ({category_counts.get('utils', 0)} files)
975
+ - **extras**: Extra libraries ({category_counts.get('extras', 0)} files)
976
+
977
+ ## Cleaning Applied
978
+ - Copyright/license headers removed
979
+ - Disabled code blocks (#if 0) removed
980
+ - Excessive trailing comments removed
981
+ - Excessive blank lines removed
982
+ - Files without actual code filtered out
983
+ - Incomplete code (missing ENDCLASS, etc.) filtered out
984
+
985
+ ## Usage for Fine-tuning
986
+ ```bash
987
+ # Using Ollama with Modelfile
988
+ FROM qwen2.5-coder:14b
989
+
990
+ # Training command
991
+ ollama create harbour-coder -f Modelfile
992
+
993
+ # Or use with other training frameworks
994
+ # The JSONL format is compatible with:
995
+ # - OpenAI fine-tuning API
996
+ # - Hugging Face transformers
997
+ # - Axolotl
998
+ # - LLaMA-Factory
999
+ ```
1000
+
1001
+ ## File Structure
1002
+ - `harbour_train.jsonl` - Training set ({len(train_entries)} entries)
1003
+ - `harbour_val.jsonl` - Validation set ({len(val_entries)} entries)
1004
+ - `harbour_dataset_full.jsonl` - Full dataset with metadata
1005
+ - `dataset_stats.json` - Dataset statistics
1006
+ - `generate_dataset.py` - This script
1007
+
1008
+ ## Source
1009
+ The source files are from the Harbour project (https://harbour.github.io/),
1010
+ an open-source Clipper-compatible compiler.
1011
+ """
1012
+
1013
+ readme_path = OUTPUT_DIR / "README.md"
1014
+ with open(readme_path, 'w', encoding='utf-8') as f:
1015
+ f.write(readme_content)
1016
+
1017
+ print(f" Saved README: {readme_path}")
1018
+
1019
+ print("\n" + "=" * 60)
1020
+ print("Dataset generation complete!")
1021
+ print("=" * 60)
1022
+
1023
+
1024
+ if __name__ == "__main__":
1025
+ main()