mirror of
https://github.com/irmen/prog8.git
synced 2026-10-22 05:02:58 +00:00
2393 lines
92 KiB
Python
Executable File
2393 lines
92 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Convert Amiga NDK LVO, SFD, and .i files to Prog8 definitions.
|
|
|
|
Usage:
|
|
python amigalibs2prog8.py <ndk_path> <output_dir> [--shared-prefix]
|
|
|
|
e.g. python scripts/amigalibs2prog8.py /path/to/AmigaNDK_headers compiler/res/prog8lib/amiga500
|
|
|
|
With --shared-prefix the files are written as shared_amiga_<library>.p8 with
|
|
%option merge, for regenerating the shared include modules in
|
|
compiler/res/prog8lib that the per-target <library>.p8 wrappers import.
|
|
|
|
Self-contained single file, clearly separated into 6 sections:
|
|
0. CONFIG - AMIGA_LIBS, TYPE_MAP, STRUCT_NAME_MAP, LIB_STRUCT_TAGS, bank map
|
|
1. DRIVER - main() controls fixed library set, two positional args
|
|
2. SCANNER - parse LVO/SFD/.i and clang JSON AST per library
|
|
3. REGISTRY - global_struct_map for cross-library typed pointers
|
|
4. TYPER - TypeResolver: struct Foo * -> ^^lib.Foo with cross-lib qualification
|
|
5. PATCHES - LIBRARY_HELPERS + Intuition GetScreen* helpers
|
|
6. WRITER - generate_structs / generate_consts with typed pointers
|
|
|
|
Parses the LVO file for LVO offsets, SFD file for function signatures,
|
|
and optionally .i files for struct definitions and constants via clang.
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Type mapping
|
|
# ---------------------------------------------------------------------------
|
|
|
|
TYPE_MAP = {
|
|
'VOID': None, 'void': None,
|
|
'LONG': 'long', 'ULONG': 'long',
|
|
'WORD': 'word', 'UWORD': 'uword',
|
|
'BYTE': 'byte', 'UBYTE': 'ubyte',
|
|
'BOOL': 'uword', # uword (not bool) for STRUCT fields: BOOL is 16-bit WORD on m68k; using 1-byte bool would read the high byte on big-endian, always seeing 0 for small values. Function returns override to 'bool' in parse_c_type (reads D0 low byte, which is correct).
|
|
'FLOAT': 'float', 'DOUBLE': 'float',
|
|
'APTR': 'pointer', 'CONST_APTR': 'pointer',
|
|
'CPTR': 'pointer', 'BPTR': 'pointer',
|
|
'STRPTR': 'str', 'CONST_STRPTR': 'str',
|
|
'TEXT': 'str', 'CONST_TEXT': 'str',
|
|
'BSTR': 'str',
|
|
'PLANEPTR': 'pointer',
|
|
'DisplayInfoHandle': 'pointer',
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Struct name mapping: assembly tag -> (C name, expected field prefix)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
STRUCT_NAME_MAP = {
|
|
# exec
|
|
'LN': ('Node', 'LN_'),
|
|
'MLN': ('MinNode', 'MLN_'),
|
|
'LH': ('List', 'LH_'),
|
|
'MLH': ('MinList', 'MLH_'),
|
|
'MP': ('MsgPort', 'MP_'),
|
|
'MN': ('Message', 'MN_'),
|
|
'IO': ('IOStdReq', 'IO_', ('IORequest', 32)),
|
|
'IS': ('Interrupt', 'IS_'),
|
|
'LIB': ('Library', 'LIB_'),
|
|
'TC_Struct': ('Task', 'TC_'),
|
|
# dos
|
|
'FileHandle': ('FileHandle', 'fh_'),
|
|
'FileLock': ('FileLock', 'fl_'),
|
|
'Process': ('Process', 'pr_'),
|
|
'DosPacket': ('DosPacket', 'dp_'),
|
|
'StandardPacket': ('StandardPacket', 'sp_'),
|
|
'DateStamp': ('DateStamp', 'date_'),
|
|
'FileInfoBlock': ('FileInfoBlock', 'fib_'),
|
|
'InfoData': ('InfoData', 'info_'),
|
|
'ErrorString': ('ErrorString', 'error_'),
|
|
'ExAllData': ('ExAllData', 'eda_'),
|
|
'ExAllControl': ('ExAllControl', 'eac_'),
|
|
# graphics
|
|
'RastPort': ('RastPort', 'rp_'),
|
|
'Layer': ('Layer', 'layer_'),
|
|
'ClipRect': ('ClipRect', 'cr_'),
|
|
'BitMap': ('BitMap', 'bm_'),
|
|
'View': ('View', 'view_'),
|
|
'TextFont': ('TextFont', 'tf_'),
|
|
'TextAttr': ('TextAttr', 'ta_'),
|
|
'ColorMap': ('ColorMap', 'cm_'),
|
|
'TmpRas': ('TmpRas', 'tr_'),
|
|
'AreaInfo': ('AreaInfo', 'ai_'),
|
|
'TextExtent': ('TextExtent', 'te_'),
|
|
'Layer_Info': ('Layer_Info', 'li_'),
|
|
# intuition
|
|
'Window': ('Window', 'wd_'),
|
|
'NewWindow': ('NewWindow', 'nw_'),
|
|
'Screen': ('Screen', 'sc_'),
|
|
'NewScreen': ('NewScreen', 'ns_'),
|
|
'IntuiMessage': ('IntuiMessage', 'im_'),
|
|
'ExtIntuiMessage':('ExtIntuiMessage', 'eim_'),
|
|
'Gadget': ('Gadget', 'gd_'),
|
|
'ExtGadget': ('ExtGadget', 'eg_'),
|
|
'Menu': ('Menu', 'mu_'),
|
|
'MenuItem': ('MenuItem', 'mi_'),
|
|
'IntuiText': ('IntuiText', 'it_'),
|
|
'Border': ('Border', 'bd_'),
|
|
'Image': ('Image', 'img_'),
|
|
'Requester': ('Requester', 'rq_'),
|
|
'EasyStruct': ('EasyStruct', 'es_'),
|
|
'DrawInfo': ('DrawInfo', 'di_'),
|
|
'IBox': ('IBox', 'ib_'),
|
|
'ColorSpec': ('ColorSpec', 'cs_'),
|
|
# utility
|
|
'TagItem': ('TagItem', 'ti_'),
|
|
'HOOK': ('Hook', 'h_'),
|
|
'CLOCKDATA': ('ClockData', ''),
|
|
# arexx (rexxsyslib)
|
|
'RexxMsg': ('RexxMsg', 'rm_'),
|
|
# iffparse
|
|
'IFFHandle': ('IFFHandle', 'iff_'),
|
|
'IFFStreamCmd': ('IFFStreamCmd', 'sc_'),
|
|
'ContextNode': ('ContextNode', 'cn_'),
|
|
'StoredProperty': ('StoredProperty', 'sp_'),
|
|
'CollectionItem': ('CollectionItem', 'ci_'),
|
|
# lowlevel
|
|
'KeyQuery': ('KeyQuery', 'kq_'),
|
|
# workbench
|
|
'DiskObject': ('DiskObject', 'do_'),
|
|
}
|
|
|
|
# Typed list link fields - keep in sync with exec.p8 hand edits and docs NDK reference
|
|
# Tail stays pointer (sentinel, not typed) even though NDK declares struct Node *lh_Tail
|
|
TYPED_LIST_FIELDS = {
|
|
'LN': {'Succ': '^^Node', 'Pred': '^^Node'},
|
|
'MLN': {'Succ': '^^MinNode', 'Pred': '^^MinNode'},
|
|
'LH': {'Head': '^^Node', 'Tail': 'pointer', 'TailPred': '^^Node'},
|
|
'MLH': {'Head': '^^MinNode', 'Tail': 'pointer', 'TailPred': '^^MinNode'},
|
|
'MN': {'Succ': '^^Message', 'Pred': '^^Message'},
|
|
}
|
|
|
|
# Prog8 keywords that conflict with field names
|
|
PROG8_KEYWORDS = {
|
|
'str', 'end', 'type', 'class', 'for', 'while', 'repeat', 'if', 'else',
|
|
'do', 'in', 'to', 'not', 'and', 'or', 'xor', 'as', 'is', 'sub',
|
|
'return', 'break', 'continue', 'void', 'true', 'false', 'pointer',
|
|
'memory', 'inline', 'private', 'step', 'alias', 'const', 'enum',
|
|
'struct', 'then', 'until', 'downto', 'when', 'goto', 'defer', 'swap',
|
|
}
|
|
|
|
PROG8_KEYWORDS.update({
|
|
'exec', 'dos', 'graphics', 'intuition',
|
|
'sys', 'main', 'txt',
|
|
})
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# .i file parsers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
RAW_TYPE_TOKENS = {'APTR','WORD','UWORD','BYTE','UBYTE','LONG','ULONG',
|
|
'BPTR','CPTR','STRPTR','CONST_APTR','CONST_STRPTR',
|
|
'TEXT','CONST_TEXT','BSTR','PLANEPTR','FLOAT','DOUBLE',
|
|
'BOOL','DisplayInfoHandle'}
|
|
|
|
|
|
def parse_struct_i_files(i_dir: str) -> dict:
|
|
"""Parse all .i files in a directory. Returns dict tag->raw_struct."""
|
|
if not os.path.isdir(i_dir):
|
|
return {}
|
|
structs = {}
|
|
for fn in sorted(os.listdir(i_dir)):
|
|
if not fn.endswith('.i'):
|
|
continue
|
|
new_structs = _parse_one_i(os.path.join(i_dir, fn))
|
|
for tag in new_structs:
|
|
if tag in structs:
|
|
print(f"Warning: struct '{tag}' redefined in {fn}, overwriting", file=sys.stderr)
|
|
structs.update(new_structs)
|
|
# Resolve total sizes for structs with base_offset references.
|
|
# (May be called again later on merged structs to resolve cross-directory bases.)
|
|
_resolve_base_sizes(structs)
|
|
return structs
|
|
|
|
|
|
def _resolve_base_sizes(structs: dict) -> None:
|
|
"""Resolve total sizes of structs that inherit from a base struct.
|
|
|
|
Iterates because some bases reference structs that also have bases.
|
|
Must be called with a dict containing all referenced base structs,
|
|
otherwise cross-directory references cannot be resolved.
|
|
"""
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
sizes = {}
|
|
for tag, rs in structs.items():
|
|
if tag in STRUCT_NAME_MAP and rs['total_size'] > 0:
|
|
tag_up = tag.upper()
|
|
sizes[f'{tag_up}_SIZE'] = rs['total_size']
|
|
sizes[f'{tag_up}_SIZEOF'] = rs['total_size']
|
|
for tag, rs in structs.items():
|
|
base_off = _parse_size_expr(rs['base_offset_str'], sizes)
|
|
expected = base_off + sum(f.get('_size', 0) for f in rs.get('fields_raw', []))
|
|
if base_off > 0 and rs['total_size'] != expected:
|
|
rs['total_size'] = expected
|
|
changed = True
|
|
if not changed:
|
|
break
|
|
|
|
|
|
def _parse_one_i(filepath: str) -> dict:
|
|
"""Parse a single .i file, return dict of tag->raw_struct."""
|
|
lines = _read_i_lines(filepath)
|
|
structs = {}
|
|
i = 0
|
|
while i < len(lines):
|
|
line = lines[i]
|
|
stripped = line.strip()
|
|
m = re.match(r'STRUCTURE\s+(\w+)\s*,\s*(\S+)', stripped)
|
|
if m:
|
|
tag = m.group(1)
|
|
base_offset_str = m.group(2)
|
|
fields = []
|
|
labels = {'_names': []}
|
|
i += 1
|
|
offset = 0
|
|
while i < len(lines):
|
|
sl = lines[i].strip()
|
|
if not sl or sl.startswith('*') or sl.startswith(';'):
|
|
i += 1
|
|
continue
|
|
if re.match(r'STRUCTURE\s', sl):
|
|
break
|
|
if re.match(r'LABEL\s+\S+', sl):
|
|
lm = re.match(r'LABEL\s+(\S+)', sl)
|
|
if lm:
|
|
labels['_names'].append(lm.group(1))
|
|
i += 1
|
|
continue
|
|
if re.match(r'\w+\s+MACRO', sl):
|
|
i += 1
|
|
while i < len(lines) and not lines[i].strip().startswith('ENDM'):
|
|
i += 1
|
|
i += 1
|
|
continue
|
|
field = _parse_field_line(sl)
|
|
if field:
|
|
if field['type'] == 'STRUCT':
|
|
sz = _parse_size_expr(field.get('size_str',''), {})
|
|
field['_offset'] = offset
|
|
field['_size'] = sz
|
|
offset += sz
|
|
else:
|
|
sz = TYPE_MAP_SIZES.get(field['type'], 4)
|
|
field['_offset'] = offset
|
|
field['_size'] = sz
|
|
offset += sz
|
|
for extra in field.get('extra_names', []):
|
|
fields.append({
|
|
'type': field['type'],
|
|
'name': extra,
|
|
'extra_names': [],
|
|
'comment': '',
|
|
'_offset': offset,
|
|
'_size': sz,
|
|
})
|
|
offset += sz
|
|
fields.append(field)
|
|
i += 1
|
|
structs[tag] = {
|
|
'tag': tag,
|
|
'base_offset_str': base_offset_str,
|
|
'fields_raw': fields,
|
|
'total_size': offset,
|
|
'labels': labels['_names'],
|
|
}
|
|
continue
|
|
i += 1
|
|
return structs
|
|
|
|
|
|
TYPE_MAP_SIZES = {
|
|
'APTR': 4, 'BPTR': 4, 'CPTR': 4, 'STRPTR': 4, 'CONST_APTR': 4, 'CONST_STRPTR': 4,
|
|
'TEXT': 4, 'CONST_TEXT': 4, 'BSTR': 4, 'PLANEPTR': 4, 'DisplayInfoHandle': 4,
|
|
'LONG': 4, 'ULONG': 4,
|
|
'WORD': 2, 'UWORD': 2,
|
|
'BYTE': 1, 'UBYTE': 1, 'BOOL': 2,
|
|
'FLOAT': 4, 'DOUBLE': 8,
|
|
'STRUCT': 0,
|
|
}
|
|
|
|
|
|
def _read_i_lines(filepath: str) -> list:
|
|
with open(filepath, encoding='latin-1') as f:
|
|
return [line.rstrip('\n').rstrip('\r') for line in f]
|
|
|
|
|
|
def _parse_field_line(line: str) -> dict | None:
|
|
stripped = line.strip()
|
|
comment = ''
|
|
for sep in ('*', ';'):
|
|
pos = stripped.find(sep)
|
|
if pos >= 0:
|
|
comment = stripped[pos:].strip()
|
|
stripped = stripped[:pos].strip()
|
|
if not stripped:
|
|
return None
|
|
m = re.match(r'STRUCT\s+(\w+)\s*,\s*(\S+)', stripped)
|
|
if m:
|
|
return {
|
|
'type': 'STRUCT',
|
|
'name': m.group(1),
|
|
'size_str': m.group(2),
|
|
'comment': comment,
|
|
}
|
|
words = stripped.split()
|
|
if not words:
|
|
return None
|
|
if words[0] in RAW_TYPE_TOKENS:
|
|
ndk_type = words[0]
|
|
field_name = words[1] if len(words) > 1 else ''
|
|
extra_names = []
|
|
if ',' in field_name:
|
|
parts = [p.strip() for p in field_name.split(',')]
|
|
field_name = parts[0]
|
|
extra_names = parts[1:]
|
|
return {
|
|
'type': ndk_type,
|
|
'name': field_name,
|
|
'extra_names': extra_names,
|
|
'comment': comment,
|
|
}
|
|
return None
|
|
|
|
|
|
def _parse_size_expr(s: str, known: dict) -> int:
|
|
"""Try to parse a size expression (number or symbol -> size)."""
|
|
try:
|
|
return int(s)
|
|
except ValueError:
|
|
if s not in known:
|
|
print(f"Warning: unknown size symbol '{s}', defaulting to 4", file=sys.stderr)
|
|
return known.get(s, 4)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Struct resolution & flattening
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def resolve_struct_sizes(raw_structs: dict) -> dict:
|
|
"""Pass 2: resolve inheritance chains and flatten each struct."""
|
|
# Build sizes dict from resolved total_size values
|
|
sizes = {}
|
|
for tag, rs in raw_structs.items():
|
|
if tag in STRUCT_NAME_MAP and rs['total_size'] > 0:
|
|
tag_up = tag.upper()
|
|
sizes[f'{tag_up}_SIZE'] = rs['total_size']
|
|
sizes[f'{tag_up}_SIZEOF'] = rs['total_size']
|
|
|
|
resolved = {}
|
|
for tag, rs in raw_structs.items():
|
|
if tag not in STRUCT_NAME_MAP:
|
|
continue
|
|
entry = STRUCT_NAME_MAP[tag]
|
|
c_name = entry[0]
|
|
prefix = entry[1]
|
|
flat = _flatten(tag, rs, raw_structs, prefix, frozenset(), sizes)
|
|
# Deduplicate field names: track used names and prefix colliding
|
|
# fields with the origin struct's C name.
|
|
used_original = {} # base name -> first origin
|
|
used_all = set() # all field names (original + renamed)
|
|
for f in flat:
|
|
base = f['prog8_name']
|
|
origin = f.get('origin_tag', tag)
|
|
origin_cname = STRUCT_NAME_MAP.get(origin, (origin,))[0]
|
|
if base not in used_original:
|
|
used_original[base] = origin
|
|
used_all.add(base)
|
|
else:
|
|
prev_origin = used_original[base]
|
|
if origin == tag:
|
|
# own field — rename the conflicting previous embedding
|
|
for prev in flat:
|
|
if prev is f:
|
|
break
|
|
if prev['prog8_name'] == base:
|
|
newname = _unique_name(f'{origin_cname}_{base}', used_all)
|
|
prev['prog8_name'] = newname
|
|
used_all.add(newname)
|
|
break
|
|
else:
|
|
newname = _unique_name(f'{origin_cname}_{base}', used_all)
|
|
f['prog8_name'] = newname
|
|
used_all.add(newname)
|
|
resolved[tag] = {
|
|
'tag': tag,
|
|
'c_name': c_name,
|
|
'prefix': prefix,
|
|
'fields': flat,
|
|
'size': rs.get('total_size', 0) or (flat[-1]['offset'] + _type_size(flat[-1]['prog8_type']) if flat else 0),
|
|
}
|
|
# If the mapping has a split, also emit the base struct
|
|
if len(entry) >= 3 and isinstance(entry[2], tuple):
|
|
base_name, base_size = entry[2]
|
|
base_fields = [f for f in flat if f['offset'] < base_size]
|
|
resolved[f'{tag}_BASE'] = {
|
|
'tag': f'{tag}_BASE',
|
|
'c_name': base_name,
|
|
'prefix': prefix,
|
|
'fields': base_fields,
|
|
'size': base_size,
|
|
}
|
|
return resolved
|
|
|
|
|
|
def _flatten(tag: str, rs, all_raw: dict, prefix: str, visiting: frozenset, sizes: dict) -> list:
|
|
if tag in visiting:
|
|
return [] # guard against cycles
|
|
new_visiting = visiting | {tag}
|
|
flat = []
|
|
offset = 0
|
|
base_off = _parse_size_expr(rs['base_offset_str'], sizes)
|
|
if base_off > 0:
|
|
parent_tag = _find_tag_by_label(rs['base_offset_str'], all_raw)
|
|
if parent_tag is None:
|
|
parent_tag = _find_tag_by_size(base_off, all_raw)
|
|
if parent_tag and parent_tag in STRUCT_NAME_MAP:
|
|
pprefix = STRUCT_NAME_MAP[parent_tag][1]
|
|
pflat = _flatten(parent_tag, all_raw[parent_tag], all_raw, pprefix, new_visiting, sizes)
|
|
flat.extend(dict(f) for f in pflat)
|
|
offset = base_off
|
|
for field in rs['fields_raw']:
|
|
if field['type'] == 'STRUCT':
|
|
sz = _parse_size_expr(field.get('size_str', ''), sizes)
|
|
if field.get('_clang_processed'):
|
|
flat.append({
|
|
'prog8_type': 'ubyte',
|
|
'prog8_name': f'emb_{field["name"]}',
|
|
'offset': offset,
|
|
'array_size': sz,
|
|
})
|
|
else:
|
|
emb_tag = _find_tag_by_size(sz, all_raw)
|
|
if emb_tag and emb_tag in STRUCT_NAME_MAP:
|
|
eprefix = STRUCT_NAME_MAP[emb_tag][1]
|
|
eflat = _flatten(emb_tag, all_raw[emb_tag], all_raw, eprefix, new_visiting, sizes)
|
|
for ef in eflat:
|
|
ec = dict(ef)
|
|
ec['offset'] += offset
|
|
flat.append(ec)
|
|
else:
|
|
flat.append({
|
|
'prog8_type': 'ubyte',
|
|
'prog8_name': f'emb_{field["name"]}',
|
|
'offset': offset,
|
|
'array_size': sz,
|
|
})
|
|
offset += sz
|
|
else:
|
|
ptype = TYPE_MAP.get(field['type'], 'pointer')
|
|
sz = field['_size']
|
|
if sz > 1:
|
|
if offset & 1:
|
|
flat.append({
|
|
'prog8_type': 'ubyte',
|
|
'prog8_name': f'_pad_{hex(offset)[2:]}',
|
|
'offset': offset,
|
|
'origin_tag': tag,
|
|
})
|
|
offset += 1
|
|
# Try cross-lib typed pointer resolution (before string heuristics)
|
|
if ptype == 'pointer' and _GLOBAL_MAP_FOR_TYPER and _CURRENT_LIB_FOR_TYPER:
|
|
qtype = field.get('_qual_type')
|
|
if qtype:
|
|
typed = _resolve_c_pointer_to_prog8(qtype, _TYPEDEF_MAP_FOR_TYPER, _CURRENT_LIB_FOR_TYPER, _GLOBAL_MAP_FOR_TYPER)
|
|
if typed:
|
|
ptype = typed
|
|
if field.get('_clang_processed'):
|
|
pname = field['name']
|
|
if field.get('_is_array'):
|
|
# For arrays of typed pointers, element size is pointer size (4)
|
|
# Apply typed list override also for arrays (unlikely but for consistency)
|
|
if tag in TYPED_LIST_FIELDS and pname in TYPED_LIST_FIELDS[tag]:
|
|
ptype = TYPED_LIST_FIELDS[tag][pname]
|
|
flat.append({
|
|
'prog8_type': ptype,
|
|
'prog8_name': pname,
|
|
'offset': offset,
|
|
'array_size': field['_size'] // _type_size(ptype),
|
|
'origin_tag': tag,
|
|
})
|
|
offset += field['_size']
|
|
continue
|
|
if ptype == 'pointer' and _is_clang_string_field(pname):
|
|
ptype = 'str'
|
|
else:
|
|
pname = _field_name(field['name'], prefix)
|
|
if ptype == 'pointer' and _is_string_field(field['name'], prefix):
|
|
ptype = 'str'
|
|
# apply typed list overrides for both clang and .i paths (keep Tail as pointer)
|
|
if tag in TYPED_LIST_FIELDS and pname in TYPED_LIST_FIELDS[tag]:
|
|
ptype = TYPED_LIST_FIELDS[tag][pname]
|
|
flat.append({
|
|
'prog8_type': ptype,
|
|
'prog8_name': pname,
|
|
'offset': offset,
|
|
'origin_tag': tag,
|
|
})
|
|
offset += sz
|
|
for extra in field.get('extra_names', []):
|
|
ename = _field_name(extra, prefix)
|
|
if sz > 1 and offset & 1:
|
|
flat.append({
|
|
'prog8_type': 'ubyte',
|
|
'prog8_name': f'_pad_{hex(offset)[2:]}',
|
|
'offset': offset,
|
|
'origin_tag': tag,
|
|
})
|
|
offset += 1
|
|
flat.append({
|
|
'prog8_type': ptype,
|
|
'prog8_name': ename,
|
|
'offset': offset,
|
|
'origin_tag': tag,
|
|
})
|
|
offset += sz
|
|
return flat
|
|
|
|
|
|
def _find_tag_by_size(sz: int, all_raw: dict) -> str | None:
|
|
for t, rs in all_raw.items():
|
|
if t in STRUCT_NAME_MAP and rs['total_size'] == sz:
|
|
return t
|
|
return None
|
|
|
|
|
|
def _find_tag_by_label(label: str, all_raw: dict) -> str | None:
|
|
"""Find the struct tag that defines a given label name (e.g. LN_SIZE -> LN)."""
|
|
for t, rs in all_raw.items():
|
|
for ln in rs.get('labels', []):
|
|
if ln.upper() == label.upper():
|
|
return t
|
|
return None
|
|
|
|
|
|
def _unique_name(base: str, used: set) -> str:
|
|
"""Find a unique name by appending numeric suffixes if needed."""
|
|
if base not in used:
|
|
return base
|
|
suffix = 1
|
|
while f'{base}{suffix}' in used:
|
|
suffix += 1
|
|
return f'{base}{suffix}'
|
|
|
|
|
|
def _type_size(prog8_type: str) -> int:
|
|
if prog8_type.startswith('^^'):
|
|
return 4
|
|
return {
|
|
'pointer': 4, 'str': 4,
|
|
'long': 4, 'uword': 2, 'word': 2,
|
|
'ubyte': 1, 'byte': 1, 'bool': 1,
|
|
'float': 4,
|
|
}.get(prog8_type, 4)
|
|
|
|
|
|
# Field name keywords that indicate an APTR is actually a string pointer
|
|
_STRING_FIELD_KEYWORDS = frozenset({
|
|
'_TITLE', '_NAME', '_TEXT', '_STRING', '_COMMENT', '_IDSTRING',
|
|
'_MATCHSTRING', '_SCREENTITLE', '_WINDOWTITLE',
|
|
})
|
|
|
|
# PascalCase equivalents for Clang field detection
|
|
_CLANG_STRING_SUFFIXES = frozenset({
|
|
'Name', 'Text', 'Title', 'String', 'Comment', 'IdString',
|
|
'MatchString', 'ScreenTitle', 'WindowTitle',
|
|
})
|
|
|
|
|
|
def _is_clang_string_field(name: str) -> bool:
|
|
"""Check if a PascalCase field name looks like a string pointer."""
|
|
return name in _CLANG_STRING_SUFFIXES or name.rstrip('_') in _CLANG_STRING_SUFFIXES
|
|
|
|
|
|
def _is_string_field(raw_field_name: str, prefix: str) -> bool:
|
|
"""Check if a field is likely a string pointer (STRPTR in C)."""
|
|
up = raw_field_name.upper()
|
|
# Strip prefix and check if the remaining name contains any string keywords
|
|
if prefix:
|
|
pu = prefix.upper()
|
|
if up.startswith(pu):
|
|
up = up[len(prefix):]
|
|
for kw in _STRING_FIELD_KEYWORDS:
|
|
# Check exact match or keyword as suffix/prefix of a compound name
|
|
if up == kw.strip('_') or up.endswith('_' + kw.strip('_')):
|
|
return True
|
|
return False
|
|
|
|
|
|
def _field_name(raw: str, prefix: str) -> str:
|
|
"""Strip the struct-specific prefix and PascalCase the result.
|
|
|
|
Prog8 struct fields are scoped, so the per-struct prefix
|
|
(e.g. LN_, IO_, nw_, wd_) is redundant.
|
|
|
|
LN_SUCC -> Succ
|
|
IO_DEVICE -> Device
|
|
IO_COMMAND -> Command
|
|
nw_LeftEdge -> LeftEdge (already PascalCase, keep as-is)
|
|
wd_RPort -> RPort (already PascalCase, keep as-is)
|
|
MN_REPLYPORT -> Replyport (all-caps, no word-boundary info)
|
|
"""
|
|
if not raw:
|
|
return 'unknown'
|
|
name = raw
|
|
if prefix:
|
|
pu = prefix.upper()
|
|
nu = name.upper()
|
|
if nu.startswith(pu):
|
|
name = name[len(prefix):]
|
|
else:
|
|
pp = prefix.rstrip('_')
|
|
if pp and nu.startswith(pp.upper()):
|
|
name = name[len(pp):]
|
|
if not name:
|
|
return 'unknown'
|
|
# PascalCase: uppercase first letter, lowercase rest for all-caps names.
|
|
# Names that already have mixed case (LeftEdge, RPort) keep their case.
|
|
if name.isupper() or name.isdigit():
|
|
name = name[0] + name[1:].lower()
|
|
else:
|
|
name = name[0].upper() + name[1:]
|
|
if name in PROG8_KEYWORDS:
|
|
name = f'k_{name}'
|
|
return name
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EQU / BITDEF parser
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def parse_consts_i_files(i_dir: str, extra_dirs: list = None, extra_files: list = None) -> list:
|
|
"""Parse .i files for constants with cross-file symbol resolution.
|
|
|
|
Collects all raw EQU definitions first, then iteratively resolves
|
|
them using a shared symbol table built from all files in i_dir,
|
|
extra_dirs (scanned for .i files), and extra_files (specific .i paths).
|
|
"""
|
|
filepaths = []
|
|
if os.path.isdir(i_dir):
|
|
for fn in sorted(os.listdir(i_dir)):
|
|
if fn.endswith('.i'):
|
|
filepaths.append(os.path.join(i_dir, fn))
|
|
if extra_dirs:
|
|
for d in extra_dirs:
|
|
if os.path.isdir(d):
|
|
for fn in sorted(os.listdir(d)):
|
|
if fn.endswith('.i'):
|
|
filepaths.append(os.path.join(d, fn))
|
|
if extra_files:
|
|
for fp in extra_files:
|
|
if os.path.isfile(fp) and fp.endswith('.i'):
|
|
filepaths.append(fp)
|
|
if not filepaths:
|
|
return []
|
|
|
|
all_resolved = []
|
|
all_raw_equ = {}
|
|
|
|
for fp in filepaths:
|
|
resolved, raw_equ = _collect_consts_one(fp)
|
|
all_resolved.extend(resolved)
|
|
for name, raw_val in raw_equ.items():
|
|
if name not in all_raw_equ:
|
|
all_raw_equ[name] = raw_val
|
|
|
|
# Build symbol table from already-resolved constants
|
|
symbol_table = dict(_KNOWN_SYMBOLS)
|
|
for c in all_resolved:
|
|
v = _eval_const(c['value'], symbol_table)
|
|
if v is not None:
|
|
symbol_table[c['name']] = v
|
|
|
|
# Iteratively resolve raw EQU defs using the shared symbol table
|
|
resolved_names = set()
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for name in list(all_raw_equ.keys()):
|
|
if name in resolved_names:
|
|
continue
|
|
v = _eval_const(all_raw_equ[name], symbol_table)
|
|
if v is not None:
|
|
all_resolved.append({'name': name, 'value': _display_value(all_raw_equ[name], v), 'prog8_type': _ct(all_raw_equ[name], v, name)})
|
|
symbol_table[name] = v
|
|
resolved_names.add(name)
|
|
changed = True
|
|
|
|
return all_resolved
|
|
|
|
|
|
def _collect_consts_one(filepath: str) -> tuple[list, dict]:
|
|
"""Parse a single .i file, return (resolved_consts, raw_equ_defs).
|
|
|
|
resolved_consts: list of already-resolved constants (BITDEF, ENUM/EITEM, simple EQU).
|
|
raw_equ_defs: dict of name -> raw_value_string for EQU defs that need cross-file resolution.
|
|
"""
|
|
resolved = []
|
|
raw_equ = {}
|
|
enum_counter = None
|
|
cmd_count = None
|
|
with open(filepath, encoding='latin-1') as f:
|
|
for line in f:
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith('*') or stripped.startswith(';'):
|
|
continue
|
|
# DEVINIT [n] — initialize the device command counter
|
|
# (macro from exec/io.i: sets CMD_COUNT to n, or to CMD_NONSTD's value if n is empty)
|
|
m = re.match(r'DEVINIT\s*(\S*)', stripped, re.IGNORECASE)
|
|
if m:
|
|
arg = m.group(1).strip()
|
|
if arg:
|
|
v = _eval_const(arg)
|
|
if v is not None:
|
|
cmd_count = v
|
|
else:
|
|
cmd_count = 0 # CMD_NONSTD not yet defined; macro defaults to 0
|
|
continue
|
|
# DEVCMD name — emit name = cmd_count, then cmd_count += 1
|
|
# (IORequest command field is UWORD, so always uword)
|
|
# Skip the MACRO definition line itself: "DEVCMD MACRO ..."
|
|
m = re.match(r'DEVCMD\s+(\w+)(?!\s*MACRO)', stripped, re.IGNORECASE)
|
|
if m and cmd_count is not None:
|
|
cname = m.group(1)
|
|
if cname.upper() == 'MACRO':
|
|
continue
|
|
resolved.append({
|
|
'name': cname,
|
|
'value': _fmt(cmd_count),
|
|
'prog8_type': 'uword',
|
|
'comment': f'; DEVCMD ({cname})'
|
|
})
|
|
cmd_count += 1
|
|
continue
|
|
m = re.match(r'BITDEF\s+(\w+)\s*,\s*(\w+)\s*,\s*(\d+)', stripped)
|
|
if m:
|
|
p = m.group(1)
|
|
n = m.group(2)
|
|
b = int(m.group(3))
|
|
resolved.append({
|
|
'name': f'{p}B_{n}',
|
|
'value': str(b),
|
|
'prog8_type': 'ubyte',
|
|
'comment': f'; BITDEF {p},{n},{b}'
|
|
})
|
|
mask = 1 << b
|
|
mask_type = 'long' if mask > 65535 else 'uword' if mask > 255 else 'ubyte'
|
|
resolved.append({
|
|
'name': f'{p}F_{n}',
|
|
'value': _fmt(mask),
|
|
'prog8_type': mask_type,
|
|
'comment': f'; BITDEF mask {p},{n},{b}'
|
|
})
|
|
continue
|
|
m = re.match(r'(\w+)\s+EQU\s+(\'(?:[^\']*)\'|\S+)', stripped, re.IGNORECASE)
|
|
if m:
|
|
cname = m.group(1)
|
|
rawv = m.group(2)
|
|
v = _eval_const(rawv)
|
|
if v is not None:
|
|
resolved.append({'name': cname, 'value': _display_value(rawv, v), 'prog8_type': _ct(rawv, v, cname)})
|
|
else:
|
|
raw_equ[cname] = rawv
|
|
continue
|
|
m = re.match(r'ENUM\s+(\S+)', stripped)
|
|
if m:
|
|
rawv = m.group(1)
|
|
v = _eval_const(rawv)
|
|
if v is not None:
|
|
enum_counter = v
|
|
continue
|
|
m = re.match(r'EITEM\s+(\w+)', stripped)
|
|
if m and enum_counter is not None:
|
|
cname = m.group(1)
|
|
resolved.append({'name': cname, 'value': _fmt(enum_counter), 'prog8_type': _ct(None, enum_counter, cname)})
|
|
enum_counter += 1
|
|
continue
|
|
return resolved, raw_equ
|
|
|
|
|
|
# Well-known symbols from .i files that aren't defined as EQU
|
|
_KNOWN_SYMBOLS = {
|
|
'TAG_USER': 0x80000000,
|
|
'TAG_USER+33': 0x80000021,
|
|
}
|
|
|
|
def _eval_const(raw: str, symbols: dict = None) -> int | None:
|
|
raw = raw.strip()
|
|
# Handle parentheses (must be before symbol lookups, as in:
|
|
# SELECTDOWN EQU (IECODE_LBUTTON) -> strip to IECODE_LBUTTON)
|
|
while raw.startswith('(') and raw.endswith(')'):
|
|
raw = raw[1:-1].strip()
|
|
# Check known symbols
|
|
if raw in _KNOWN_SYMBOLS:
|
|
return _KNOWN_SYMBOLS[raw]
|
|
# Check dynamic symbol table
|
|
if symbols is not None and raw in symbols:
|
|
return symbols[raw]
|
|
# Handle simple binary expressions: X + Y, X - Y
|
|
for op in ('+', '-'):
|
|
parts = raw.split(op, 1)
|
|
if len(parts) == 2:
|
|
a = _eval_const(parts[0].strip(), symbols)
|
|
b = _eval_const(parts[1].strip(), symbols)
|
|
if a is not None and b is not None:
|
|
return a + b if op == '+' else a - b
|
|
# Handle bitwise OR/AND ('!' is the m68k assembler OR operator)
|
|
for op in ('!', '&'):
|
|
parts = raw.split(op, 1)
|
|
if len(parts) == 2:
|
|
a = _eval_const(parts[0].strip(), symbols)
|
|
b = _eval_const(parts[1].strip(), symbols)
|
|
if a is not None and b is not None:
|
|
return a | b if op == '!' else a & b
|
|
# Handle bit shift with optional type suffix
|
|
m = re.match(r'(\d+)(?:UL|L|U)?\s*<<\s*(\d+)', raw)
|
|
if m:
|
|
return int(m.group(1)) << int(m.group(2))
|
|
# Handle multi-char constants: 'FORM' -> 0x464F524D (big-endian)
|
|
m = re.match(r"'(.+?)'", raw)
|
|
if m and len(m.group(1)) <= 4:
|
|
v = 0
|
|
for ch in m.group(1):
|
|
v = (v << 8) | ord(ch)
|
|
return v
|
|
# Handle hex with C-style (0x) or assembler-style ($) prefix
|
|
m = re.match(r'(?:0x|\$)([0-9a-fA-F]+)', raw)
|
|
if m:
|
|
return int(m.group(1), 16)
|
|
m = re.match(r'(-?\d+)', raw)
|
|
if m:
|
|
return int(m.group(1))
|
|
return None
|
|
|
|
|
|
def _fmt(v: int) -> str:
|
|
if v < 0:
|
|
return str(v)
|
|
return f'${v:04x}' if v <= 65535 else f'${v:08x}' if v > 255 else f'${v:02x}'
|
|
|
|
|
|
def _display_value(rawv, v: int) -> str:
|
|
"""The value string to emit for a constant.
|
|
|
|
When the EQU is written as a plain hex literal (e.g. $0000000f), the
|
|
original literal is preserved verbatim so the generated width matches the
|
|
NDK source. Otherwise the value is formatted from its integer result.
|
|
"""
|
|
s = (rawv or '').strip()
|
|
while s.startswith('(') and s.endswith(')'):
|
|
s = s[1:-1].strip()
|
|
if re.fullmatch(r'(?:0x|\$)[0-9a-fA-F]+', s):
|
|
return s
|
|
return _fmt(v)
|
|
|
|
|
|
# Constant prefixes that should always be 'long' (LONG flags fields in Amiga structs)
|
|
_LONG_CONST_PREFIXES = ('IDCMP_', 'WFLG_', 'GFLG_', 'GACT_', 'GTYP_', 'GMORE_')
|
|
# Constant prefixes that should always be 'uword' (UWORD fields like IntuiMessage.Code/Qualifier)
|
|
_UWORD_CONST_PREFIXES = ('IECODE_', 'IECODEB_', 'IECLASS_', 'IESUBCLASS_',
|
|
'IEQUALIFIER_', 'IEQUALIFIERB_')
|
|
# Specific names that must be 'uword' (mouse button codes matching Code field)
|
|
_UWORD_CONST_NAMES = {'SELECTDOWN', 'SELECTUP', 'MENUDOWN', 'MENUUP', 'MIDDLEDOWN', 'MIDDLEUP'}
|
|
|
|
def _ct(rawv, v: int, name: str = '') -> str:
|
|
# A constant written as a fixed-width hex literal keeps that width as
|
|
# its Prog8 type, even when the numeric value would fit in a smaller type.
|
|
# $xxxx -> uword
|
|
# $xxxxxxxx -> long
|
|
if rawv:
|
|
s = rawv.strip()
|
|
while s.startswith('(') and s.endswith(')'):
|
|
s = s[1:-1].strip()
|
|
hm = re.fullmatch(r'(?:0x|\$)([0-9a-fA-F]+)', s)
|
|
if hm:
|
|
digits = len(hm.group(1))
|
|
if digits == 8:
|
|
return 'long'
|
|
if digits == 4:
|
|
return 'uword'
|
|
if any(name.startswith(p) for p in _LONG_CONST_PREFIXES):
|
|
return 'long'
|
|
if any(name.startswith(p) for p in _UWORD_CONST_PREFIXES):
|
|
return 'uword'
|
|
if name in _UWORD_CONST_NAMES:
|
|
return 'uword'
|
|
if v < 0:
|
|
return 'long'
|
|
return 'ubyte' if v <= 255 else 'uword' if v <= 65535 else 'long'
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Prog8 code generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def generate_structs(resolved: dict, lib_name: str, indent: str = ' ', header_text: str | None = None,
|
|
max_size: int = 256) -> str:
|
|
lines = []
|
|
if not resolved:
|
|
return ''
|
|
header = header_text if header_text else f'struct definitions for {lib_name}'
|
|
lines.append(f'\n{indent}; ---- {header} ----')
|
|
for tag in sorted(resolved.keys()):
|
|
lines.append('')
|
|
rs = resolved[tag]
|
|
fields = rs['fields']
|
|
total_size = rs['size']
|
|
stripped = []
|
|
# struct truncation override: strip everything after named field (field itself is kept)
|
|
truncate_after = STRUCT_TRUNCATE_AFTER.get(rs["c_name"])
|
|
if truncate_after:
|
|
for fi, f in enumerate(fields):
|
|
if f['prog8_name'] == truncate_after and f.get('origin_tag', tag) == tag:
|
|
to_strip = fields[fi+1:]
|
|
for fs in to_strip:
|
|
fsz = fs.get('array_size', 1) * _type_size(fs['prog8_type'])
|
|
total_size -= fsz
|
|
stripped.append((fs, fsz))
|
|
del fields[fi+1:]
|
|
break
|
|
# strip trailing reserved fields if struct exceeds max_size
|
|
if total_size > max_size:
|
|
for fi in range(len(fields) - 1, -1, -1):
|
|
f = fields[fi]
|
|
fsize = f.get('array_size', 1) * _type_size(f['prog8_type'])
|
|
if 'reserved' in f['prog8_name'].lower():
|
|
total_size -= fsize
|
|
stripped.append((f, fsize))
|
|
del fields[fi]
|
|
if total_size <= max_size:
|
|
break
|
|
# if still too large, strip opaque embedded structs (ubyte[N]) from the end
|
|
if total_size > max_size:
|
|
for fi in range(len(fields) - 1, -1, -1):
|
|
f = fields[fi]
|
|
fsize = f.get('array_size', 1) * _type_size(f['prog8_type'])
|
|
if f['prog8_type'] == 'ubyte' and f.get('array_size', 1) > 4:
|
|
total_size -= fsize
|
|
stripped.append((f, fsize))
|
|
del fields[fi]
|
|
if total_size <= max_size:
|
|
break
|
|
# recompute total_size from remaining fields
|
|
if fields:
|
|
last = max(fields, key=lambda f: f['offset'])
|
|
last_end = last['offset'] + (last.get('array_size', 1) * _type_size(last['prog8_type']))
|
|
total_size = last_end
|
|
if total_size & 1:
|
|
total_size += 1 # word-align
|
|
else:
|
|
total_size = 0
|
|
lines.append(f'{indent}struct {rs["c_name"]} {{ ; total size: {total_size}')
|
|
for f in fields:
|
|
ptype = f['prog8_type']
|
|
pname = f['prog8_name']
|
|
off = f['offset']
|
|
comment = f.get('comment', '')
|
|
arr_size = f.get('array_size')
|
|
if arr_size:
|
|
line = f'{indent} {ptype}[{arr_size}] {pname}'
|
|
else:
|
|
line = f'{indent} {ptype} {pname}'
|
|
if comment:
|
|
line += f' {comment}'
|
|
line += f' ; {off}'
|
|
lines.append(line)
|
|
if stripped:
|
|
stripped.reverse()
|
|
parts = ', '.join(
|
|
(f'{f["prog8_type"]}[{f["array_size"]}] ' if f.get("array_size") else f'{f["prog8_type"]} ')
|
|
+ f'{f["prog8_name"]} ({fz}B)'
|
|
for f, fz in stripped
|
|
)
|
|
lines.append(f'{indent}; stripped: {parts}')
|
|
lines.append(f'{indent}}}')
|
|
return '\n'.join(lines)
|
|
|
|
|
|
def generate_consts(consts: list, lib_name: str, indent: str = ' ', header_text: str | None = None) -> str:
|
|
if not consts:
|
|
return ''
|
|
header = header_text if header_text else f'constants for {lib_name}'
|
|
lines = [f'\n{indent}; ---- {header} ----']
|
|
seen = set()
|
|
for c in consts:
|
|
if c['name'] in seen:
|
|
continue
|
|
seen.add(c['name'])
|
|
line = f'{indent}const {c["prog8_type"]} {c["name"]} = {c["value"]}'
|
|
lines.append(line)
|
|
return '\n'.join(lines)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Struct truncation overrides
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Struct tag -> field name: truncate struct after this field (field itself is kept).
|
|
# Fields after the named field are stripped with a comment.
|
|
STRUCT_TRUNCATE_AFTER = {
|
|
'Screen': 'Font', # Truncate before embedded ViewPort (offset 44), use GetScreen* helpers for ViewPort/RastPort/BitMap
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Library bank mapping (unchanged)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
LIBRARY_BANK_MAP = {
|
|
'exec': 1, 'dos': 2, 'graphics': 3, 'intuition': 4,
|
|
'gadtools': 5, 'layers': 6, 'asl': 7, 'console': 8,
|
|
'utility': 9, 'expansion': 10, 'icon': 11, 'wb': 12,
|
|
'diskfont': 13, 'iffparse': 14, 'input': 15, 'keymap': 16,
|
|
'locale': 17, 'timer': 18, 'lowlevel': 19, 'mathffp': 20,
|
|
'mathieeesingbas': 21, 'mathieeesingtrans': 22,
|
|
'mathieeedoubbas': 23, 'mathieeedoubtrans': 24,
|
|
'mathtrans': 25, 'nonvolatile': 26, 'realtime': 27,
|
|
'translator': 28, 'rexxsyslib': 29, 'commodities': 30,
|
|
'datatypes': 31, 'disk': 32, 'amigaguide': 33, 'arexx': 34,
|
|
'battclock': 35, 'battmem': 36, 'bevel': 37, 'bitmap': 38,
|
|
'bullet': 39, 'button': 40, 'cardres': 41, 'checkbox': 42,
|
|
'chooser': 43, 'cia': 44, 'clicktab': 45, 'colorwheel': 46,
|
|
'datebrowser': 47, 'drawlist': 48, 'dtclass': 49,
|
|
'fuelgauge': 50, 'getcolor': 51, 'getfile': 52, 'getfont': 53,
|
|
'getscreenmode': 54, 'glyph': 55, 'integer': 56, 'label': 57,
|
|
'layout': 58, 'listbrowser': 59, 'misc': 60, 'palette': 61,
|
|
'penmap': 62, 'potgo': 63, 'radiobutton': 64, 'ramdrive': 65,
|
|
'requester': 66, 'scroller': 67, 'sketchboard': 68,
|
|
'slider': 69, 'space': 70, 'speedbar': 71, 'string': 72,
|
|
'texteditor': 73, 'trackfile': 74, 'virtual': 75, 'window': 76,
|
|
}
|
|
|
|
# Note: library names (e.g. 'icon', 'exec') are NOT added to PROG8_KEYWORDS.
|
|
# They are namespaced identifiers, not reserved words. Adding them would cause
|
|
# SFD parameter renaming bugs (e.g. the icon library's own param 'icon' would
|
|
# become 'k_icon') and struct field over-prefixing.
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 0. CONFIG - Amiga libraries to generate
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# NOTE: diskfont is intentionally NOT auto-generated: shared_amiga_diskfont.p8 is
|
|
# hand-maintained, trimmed to pre-2.0 content (the SFD contains post-2.0
|
|
# outline-engine, diskfont-control and tagged-font APIs).
|
|
AMIGA_LIBS = ["exec", "dos", "graphics", "intuition", "utility", "iffparse", "arexx", "icon", "lowlevel"]
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Library-specific helper routines (not from NDK, hand-maintained)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
LIBRARY_HELPERS = {
|
|
'dos': '''
|
|
sub read_file(str filename, bool chip_ram) -> pointer, long {
|
|
; Loads an entire file into freshly allocated memory.
|
|
; Returns a pointer to the data and the size, or 0,0 on error.
|
|
; You should call exec.FreeMem() on the pointer when done.
|
|
pointer lock = dos.Lock(filename, dos.ACCESS_READ)
|
|
if lock == 0 return 0, 0
|
|
defer void dos.UnLock(lock)
|
|
|
|
; the FileInfoBlock must be longword aligned; AllocMem guarantees that
|
|
; (also makes this routine reentrant unlike a static struct instance)
|
|
^^dos.FileInfoBlock fib = exec.AllocMem(sizeof(dos.FileInfoBlock) as long, exec.MEMF_PUBLIC | exec.MEMF_CLEAR) as^^ dos.FileInfoBlock
|
|
if fib == 0
|
|
return 0, 0
|
|
defer exec.FreeMem(fib as pointer, sizeof(dos.FileInfoBlock) as long)
|
|
if dos.Examine(lock, fib) == 0
|
|
return 0, 0
|
|
if fib.DirEntryType > 0
|
|
return 0, 0 ; a directory is not a file
|
|
long size = fib.Size
|
|
if size <= 0
|
|
return 0, 0 ; zero-size files are an error
|
|
|
|
pointer fh = dos.Open(filename, dos.MODE_OLDFILE)
|
|
if fh == 0
|
|
return 0, 0
|
|
defer void dos.Close(fh)
|
|
|
|
long memflags = exec.MEMF_PUBLIC
|
|
if chip_ram
|
|
memflags |= exec.MEMF_CHIP
|
|
pointer data = exec.AllocMem(size, memflags)
|
|
if data == 0
|
|
return 0, 0
|
|
|
|
; dos.Read may return short counts; loop until the full file is in
|
|
; memory. n <= 0 means error or premature end of file.
|
|
long done = 0
|
|
while done < size {
|
|
long n = dos.Read(fh, data + done, size - done)
|
|
if n <= 0 {
|
|
exec.FreeMem(data, size)
|
|
return 0, 0
|
|
}
|
|
done += n
|
|
}
|
|
return data, size
|
|
}
|
|
''',
|
|
'graphics': '''
|
|
; Friendly aliases for the ViewPort.Modes bits from view.h.
|
|
; The NDK .i files only provide the V_-prefixed EQU variants
|
|
; (V_HIRES, V_LACE); these match the names used in C examples.
|
|
const uword HIRES = $8000
|
|
const uword LORES = 0
|
|
const ubyte LACE = $0004
|
|
const ubyte SUPERHIRES = $20
|
|
''',
|
|
'exec': '''
|
|
asmsub NewList(^^List list @A0) {
|
|
; manually added utility routine to initialize new exec linked lists
|
|
%asm {{
|
|
addq.l #4, a0 ; Point to lh_Tail
|
|
move.l a0, -4(a0) ; lh_Head = &lh_Tail
|
|
clr.l (a0) ; lh_Tail = NULL
|
|
subq.l #4, a0 ; Point back to &lh_Head
|
|
move.l a0, 8(a0) ; lh_TailPred = &lh_Head
|
|
rts
|
|
}}
|
|
}
|
|
''',
|
|
'arexx': '''
|
|
sub openlib() -> bool {
|
|
sys.RexxSysBase = exec.OpenLibrary("rexxsyslib.library", 0)
|
|
return sys.RexxSysBase!=0
|
|
}
|
|
|
|
sub closelib() {
|
|
if sys.RexxSysBase!=0 {
|
|
exec.CloseLibrary(sys.RexxSysBase)
|
|
sys.RexxSysBase = 0
|
|
}
|
|
}
|
|
''',
|
|
'iffparse': '''
|
|
sub openlib() -> bool {
|
|
sys.IFFParseBase = exec.OpenLibrary("iffparse.library", 0)
|
|
return sys.IFFParseBase!=0
|
|
}
|
|
|
|
sub closelib() {
|
|
if sys.IFFParseBase!=0 {
|
|
exec.CloseLibrary(sys.IFFParseBase)
|
|
sys.IFFParseBase = 0
|
|
}
|
|
}
|
|
''',
|
|
'lowlevel': '''
|
|
sub openlib() -> bool {
|
|
; not opened at startup: lowlevel.library only exists on kickstart 2.0+,
|
|
; so it must be opened on demand and may legitimately fail
|
|
sys.LowLevelBase = exec.OpenLibrary("lowlevel.library", 0)
|
|
return sys.LowLevelBase!=0
|
|
}
|
|
|
|
sub closelib() {
|
|
if sys.LowLevelBase!=0 {
|
|
exec.CloseLibrary(sys.LowLevelBase)
|
|
sys.LowLevelBase = 0
|
|
}
|
|
}
|
|
''',
|
|
'intuition': '''
|
|
inline asmsub GetScreenViewPort(^^Screen screen @A0) -> pointer @A1 {
|
|
; ViewPort is embedded at Screen offset 44; the ViewPort struct is not generated
|
|
%asm {{
|
|
lea 44(a0), a1
|
|
}}
|
|
}
|
|
|
|
inline asmsub GetScreenRastPort(^^Screen screen @A0) -> ^^graphics.RastPort @A1 {
|
|
; RastPort is embedded at Screen offset 84
|
|
%asm {{
|
|
lea 84(a0), a1
|
|
}}
|
|
}
|
|
|
|
inline asmsub GetScreenBitMap(^^Screen screen @A0) -> ^^graphics.BitMap @A1 {
|
|
; BitMap is embedded at Screen offset 184
|
|
%asm {{
|
|
lea 184(a0), a1
|
|
}}
|
|
}
|
|
''',
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Existing SFD / LVO parsers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def parse_c_type(c_type: str) -> tuple:
|
|
c_type = c_type.strip()
|
|
c_type = re.sub(r'\b(const|volatile|register|unsigned|signed)\b', '', c_type).strip()
|
|
while ' ' in c_type:
|
|
c_type = c_type.replace(' ', ' ')
|
|
is_ptr = '*' in c_type
|
|
c_type = c_type.replace('*', '').strip()
|
|
c_type = re.sub(r'\b(struct|union)\s+', '', c_type).strip()
|
|
if c_type in TYPE_MAP:
|
|
# If the original was a pointer, return pointer (or str for char*).
|
|
if is_ptr:
|
|
if c_type in ('STRPTR', 'TEXT', 'CONST_STRPTR', 'CONST_TEXT', 'BSTR', 'char'):
|
|
return ('str', c_type)
|
|
if c_type == 'BOOL':
|
|
return ('pointer', c_type)
|
|
return ('pointer', c_type)
|
|
prog8_type = TYPE_MAP[c_type]
|
|
if c_type == 'BOOL':
|
|
prog8_type = 'bool'
|
|
return (prog8_type, c_type)
|
|
return ('pointer', c_type) if is_ptr else ('long', c_type)
|
|
|
|
|
|
def parse_lvo(filepath: str) -> dict:
|
|
lvos = {}
|
|
with open(filepath, encoding='latin-1') as f:
|
|
for line in f:
|
|
m = re.match(r'^_LVO(\w+)\s+equ\s+(-?\d+)', line)
|
|
if m:
|
|
lvos[m.group(1)] = int(m.group(2))
|
|
return lvos
|
|
|
|
|
|
def join_sfd_lines(filepath: str) -> list:
|
|
joined = []
|
|
with open(filepath, encoding='latin-1') as f:
|
|
prev = None
|
|
for line in f:
|
|
line = line.rstrip('\n')
|
|
if prev is not None and (line.startswith('\t') or line.startswith(' ')):
|
|
prev += ' ' + line.lstrip()
|
|
else:
|
|
if prev is not None:
|
|
joined.append(prev)
|
|
prev = line
|
|
if prev is not None:
|
|
joined.append(prev)
|
|
return joined
|
|
|
|
|
|
def extract_last_paren_group(text: str):
|
|
close_pos = -1
|
|
for i in range(len(text) - 1, -1, -1):
|
|
if text[i] == ')':
|
|
close_pos = i
|
|
break
|
|
if close_pos < 0:
|
|
return None
|
|
depth = 0
|
|
open_pos = -1
|
|
for i in range(close_pos, -1, -1):
|
|
if text[i] == ')':
|
|
depth += 1
|
|
elif text[i] == '(':
|
|
depth -= 1
|
|
if depth == 0:
|
|
open_pos = i
|
|
break
|
|
if open_pos < 0:
|
|
return None
|
|
return (open_pos, close_pos, text[open_pos + 1:close_pos].strip())
|
|
|
|
|
|
def parse_sfd_param(param_str: str) -> tuple:
|
|
param_str = param_str.strip()
|
|
if not param_str:
|
|
return ('long', 'arg')
|
|
fp = re.search(r'\(\*(\w+)\)', param_str)
|
|
if fp:
|
|
return ('pointer', fp.group(1))
|
|
parts = param_str.rsplit(None, 1)
|
|
if len(parts) == 2:
|
|
ptype_raw, pname = parts
|
|
else:
|
|
ptype_raw = parts[0]
|
|
pname = 'arg'
|
|
while pname.startswith('*'):
|
|
ptype_raw += ' *'; pname = pname[1:]
|
|
if not pname:
|
|
return ('long', 'arg')
|
|
prog8_type = parse_c_type(ptype_raw)[0] or 'long'
|
|
# Try typed struct pointer if global registry is available (cross-lib)
|
|
if prog8_type == 'pointer' and _GLOBAL_MAP_FOR_TYPER and _CURRENT_LIB_FOR_TYPER:
|
|
typed = _resolve_c_pointer_to_prog8(ptype_raw, _TYPEDEF_MAP_FOR_TYPER, _CURRENT_LIB_FOR_TYPER, _GLOBAL_MAP_FOR_TYPER)
|
|
if typed:
|
|
prog8_type = 'pointer' if _is_text_extent_pointer(typed) or _is_tag_item_pointer(typed) else typed
|
|
if pname in PROG8_KEYWORDS:
|
|
pname = f'k_{pname}'
|
|
return (prog8_type, pname)
|
|
|
|
|
|
def _is_text_extent_pointer(prog8_type: str) -> bool:
|
|
return prog8_type in ('^^TextExtent', '^^graphics.TextExtent')
|
|
|
|
|
|
def _is_tag_item_pointer(prog8_type: str) -> bool:
|
|
# Tag lists are commonly flat arrays, which Prog8 cannot represent as
|
|
# arrays of struct instances.
|
|
return prog8_type in ('^^TagItem', '^^utility.TagItem')
|
|
|
|
|
|
def parse_sfd(filepath: str) -> dict:
|
|
lines = join_sfd_lines(filepath)
|
|
funcs = {}
|
|
in_alias = False
|
|
libname = ''
|
|
for line in lines:
|
|
stripped = line.strip()
|
|
if not stripped or stripped.startswith('*'):
|
|
in_alias = False
|
|
continue
|
|
if stripped.startswith('==') or stripped.startswith('##'):
|
|
d = stripped.split()[0]
|
|
if d in ('==alias', '##alias'):
|
|
in_alias = True
|
|
continue
|
|
in_alias = False
|
|
m = re.match(r'==base\s+(\S+)', stripped)
|
|
if m:
|
|
libname = m.group(1)
|
|
continue
|
|
if in_alias:
|
|
in_alias = False
|
|
continue
|
|
info = parse_sfd_func_line(line)
|
|
if info: funcs[info['name']] = info
|
|
funcs['__libname'] = libname
|
|
return funcs
|
|
|
|
|
|
def split_sfd_params(param_str: str) -> list:
|
|
"""Split SFD parameters by comma, respecting nested parentheses."""
|
|
parts = []
|
|
depth = 0
|
|
current = []
|
|
for ch in param_str:
|
|
if ch == '(':
|
|
depth += 1
|
|
current.append(ch)
|
|
elif ch == ')':
|
|
depth -= 1
|
|
current.append(ch)
|
|
elif ch == ',' and depth == 0:
|
|
parts.append(''.join(current).strip())
|
|
current = []
|
|
else:
|
|
current.append(ch)
|
|
if current:
|
|
parts.append(''.join(current).strip())
|
|
return [p for p in parts if p]
|
|
|
|
|
|
def parse_sfd_func_line(line: str):
|
|
line = line.strip()
|
|
if not line:
|
|
return None
|
|
rg = extract_last_paren_group(line)
|
|
if rg is None:
|
|
return None
|
|
regs = [r.strip() for r in rg[2].replace('/', ',').split(',') if r.strip()] if rg[2] else []
|
|
sig = line[:rg[0]].strip()
|
|
if not sig:
|
|
return None
|
|
fp = sig.find('(')
|
|
if fp < 0:
|
|
return None
|
|
depth = 0
|
|
pc = -1
|
|
for i in range(fp, len(sig)):
|
|
if sig[i] == '(':
|
|
depth += 1
|
|
elif sig[i] == ')':
|
|
depth -= 1
|
|
if depth == 0:
|
|
pc = i
|
|
break
|
|
if pc < 0:
|
|
return None
|
|
ps = sig[fp+1:pc].strip()
|
|
before = sig[:fp].strip()
|
|
words = before.split()
|
|
if not words:
|
|
return None
|
|
nraw = words[-1]
|
|
name = nraw.lstrip('*')
|
|
stars = len(nraw) - len(name)
|
|
rt = ' '.join(words[:-1]) + (' *' * stars) if stars > 0 else ' '.join(words[:-1])
|
|
is_void = rt.strip() in ('VOID', 'void')
|
|
params = [parse_sfd_param(p) for p in split_sfd_params(ps)] if ps else []
|
|
return {'name': name, 'ret_type': rt, 'params': params, 'regs': regs, 'is_void': is_void}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Clang JSON AST based struct parser (replaces .i file parsing)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Mapping from C qualType (as emitted by clang) to NDK type tokens
|
|
_C_QUAL_TO_NDK = {
|
|
'APTR': ('APTR', 4),
|
|
'CONST_APTR': ('CONST_APTR', 4),
|
|
'BPTR': ('BPTR', 4),
|
|
'CPTR': ('CPTR', 4),
|
|
'STRPTR': ('STRPTR', 4),
|
|
'CONST_STRPTR': ('CONST_STRPTR', 4),
|
|
'PLANEPTR': ('PLANEPTR', 4),
|
|
'DisplayInfoHandle': ('DisplayInfoHandle', 4),
|
|
'BSTR': ('BSTR', 4),
|
|
'UBYTE': ('UBYTE', 1),
|
|
'BYTE': ('BYTE', 1),
|
|
'UWORD': ('UWORD', 2),
|
|
'WORD': ('WORD', 2),
|
|
'ULONG': ('ULONG', 4),
|
|
'LONG': ('LONG', 4),
|
|
'BOOL': ('BOOL', 2),
|
|
'FLOAT': ('FLOAT', 4),
|
|
'DOUBLE': ('DOUBLE', 8),
|
|
'TEXT': ('UBYTE', 1),
|
|
'CONST_TEXT': ('UBYTE', 1),
|
|
'char': ('UBYTE', 1),
|
|
'unsigned char': ('UBYTE', 1),
|
|
'signed char': ('BYTE', 1),
|
|
}
|
|
|
|
|
|
def _c_field_type_to_ndk(qual_type: str, typedef_map: dict) -> tuple:
|
|
"""Convert a C qualType string to (ndk_type, size_in_bytes)."""
|
|
qt = qual_type.strip()
|
|
|
|
# Pointer types: anything with * or function pointer syntax
|
|
if '*' in qt or '(**' in qt or '(*)(' in qt:
|
|
# char * is a string pointer
|
|
if qt.startswith('char ') or qt.startswith('const char '):
|
|
return ('STRPTR', 4)
|
|
if 'char' in qt.replace('*', '').strip():
|
|
return ('STRPTR', 4)
|
|
return ('APTR', 4)
|
|
|
|
# Function pointers like void (*)()
|
|
if qt.startswith('void (*') or qt.startswith('void (*'):
|
|
return ('APTR', 4)
|
|
|
|
# Embedded structs/unions: struct Name (without *)
|
|
if qt.startswith('struct ') or qt.startswith('union '):
|
|
return ('STRUCT', 0)
|
|
|
|
# Arrays: BaseType[N]
|
|
m = re.match(r'(\S+)\[(\d+)\]', qt)
|
|
if m:
|
|
base_qt = m.group(1)
|
|
count = int(m.group(2))
|
|
base_ndk, base_size = _c_field_type_to_ndk(base_qt, typedef_map)
|
|
return (base_ndk, base_size * count)
|
|
|
|
# Void is unusual for a field but handle it
|
|
if qt == 'void':
|
|
return ('APTR', 4)
|
|
|
|
# Try direct NDK type lookup
|
|
if qt in _C_QUAL_TO_NDK:
|
|
return _C_QUAL_TO_NDK[qt]
|
|
|
|
# Resolve typedef chain
|
|
resolved = qt
|
|
seen = set()
|
|
while resolved in typedef_map and resolved not in seen:
|
|
seen.add(resolved)
|
|
resolved = typedef_map[resolved]
|
|
if resolved in _C_QUAL_TO_NDK:
|
|
return _C_QUAL_TO_NDK[resolved]
|
|
|
|
# Try cleaning up qualifiers
|
|
cleaned = re.sub(r'\b(const|volatile|unsigned|signed)\b', '', resolved).strip()
|
|
cleaned = re.sub(r'\s+', ' ', cleaned)
|
|
if cleaned in _C_QUAL_TO_NDK:
|
|
return _C_QUAL_TO_NDK[cleaned]
|
|
|
|
# Fallback: 4 bytes as APTR
|
|
return ('APTR', 4)
|
|
|
|
|
|
def _c_field_name_to_asm(c_name: str, prefix: str) -> str:
|
|
"""Convert a C field name to Prog8 field name.
|
|
|
|
Strips the C-style lowercase prefix (e.g. 'ln_', 'lh_', 'tc_')
|
|
and PascalCases the result: first char uppercase, rest lowercase.
|
|
Handles special case where l_pad doesn't match prefix lh_ (falls back to l_).
|
|
"""
|
|
if not c_name:
|
|
return 'unknown'
|
|
|
|
pu = prefix.upper()
|
|
lower_name = c_name.lower()
|
|
|
|
# Try full prefix match first
|
|
c_prefix = prefix.lower().rstrip('_')
|
|
if c_prefix and lower_name.startswith(c_prefix + '_'):
|
|
rest = c_name[len(c_prefix) + 1:]
|
|
elif lower_name.startswith(prefix.lower()):
|
|
rest = c_name[len(prefix):]
|
|
else:
|
|
# Try stripping the first underscore-delimited component
|
|
parts = c_name.split('_', 1)
|
|
if len(parts) == 2:
|
|
rest = parts[1]
|
|
else:
|
|
rest = c_name
|
|
|
|
if not rest:
|
|
return 'unknown'
|
|
|
|
# Preserve existing mixed case (already PascalCase like RPort, TxHeight)
|
|
# Only PascalCase if all-uppercase (like REPLYPE) or all-lowercase (like replype)
|
|
if rest.isupper() or rest.islower():
|
|
result = rest[0].upper() + rest[1:].lower()
|
|
else:
|
|
result = rest # Keep existing mixed case
|
|
|
|
# Ensure the name doesn't start with a digit (invalid in Prog8)
|
|
if result and result[0].isdigit():
|
|
# Prefix with the first component of the original C name
|
|
prefix_part = c_name.split('_')[0]
|
|
# Keep PascalCase for the prefix part
|
|
prefix_part = prefix_part[0].upper() + prefix_part[1:].lower()
|
|
result = prefix_part + '_' + result
|
|
|
|
return result
|
|
|
|
|
|
# 68000 word-alignment rule: align to min(type_size, 2)
|
|
_ALIGNMENT = lambda s: min(s, 2)
|
|
|
|
|
|
def _align_offset(offset: int, size: int) -> int:
|
|
align = _ALIGNMENT(size)
|
|
if align > 0 and offset % align != 0:
|
|
offset += align - (offset % align)
|
|
return offset
|
|
|
|
|
|
def _build_typedef_map(ast: dict) -> dict:
|
|
"""Build a mapping from typedef name to underlying qualType from the JSON AST."""
|
|
typedef_map = {}
|
|
|
|
def walk(node):
|
|
if isinstance(node, dict):
|
|
if node.get('kind') == 'TypedefDecl':
|
|
name = node.get('name')
|
|
qtype = node.get('type', {}).get('qualType')
|
|
if name and qtype and '?' not in qtype and qtype != name:
|
|
typedef_map[name] = qtype
|
|
for val in node.values():
|
|
if isinstance(val, (dict, list)):
|
|
walk(val)
|
|
elif isinstance(node, list):
|
|
for item in node:
|
|
walk(item)
|
|
|
|
walk(ast)
|
|
return typedef_map
|
|
|
|
|
|
def _find_record_decl(ast: dict, name: str) -> dict | None:
|
|
"""Find a RecordDecl (struct definition) by name, preferring the definition over forward declarations."""
|
|
|
|
found = []
|
|
|
|
def search(node):
|
|
if isinstance(node, dict):
|
|
if node.get('kind') == 'RecordDecl' and node.get('name') == name:
|
|
found.append(node)
|
|
for val in node.values():
|
|
if isinstance(val, (dict, list)):
|
|
search(val)
|
|
elif isinstance(node, list):
|
|
for item in node:
|
|
search(item)
|
|
|
|
search(ast)
|
|
|
|
# Prefer definition with fields
|
|
for d in found:
|
|
if d.get('isDefinition') and any(c.get('kind') == 'FieldDecl' for c in d.get('inner', [])):
|
|
return d
|
|
# Then any definition
|
|
for d in found:
|
|
if d.get('isDefinition'):
|
|
return d
|
|
# Then any with fields
|
|
for d in found:
|
|
if any(c.get('kind') == 'FieldDecl' for c in d.get('inner', [])):
|
|
return d
|
|
# Otherwise return first
|
|
return found[0] if found else None
|
|
|
|
|
|
def _get_struct_fields(record_decl: dict) -> list:
|
|
"""Extract FieldDecl entries from a RecordDecl."""
|
|
fields = []
|
|
for child in record_decl.get('inner', []):
|
|
if child.get('kind') == 'FieldDecl':
|
|
name = child.get('name', '')
|
|
qtype = child.get('type', {}).get('qualType', '')
|
|
if name and qtype:
|
|
fields.append((name, qtype))
|
|
return fields
|
|
|
|
|
|
def _find_tag_by_c_name(c_name: str) -> str | None:
|
|
"""Find the assembly tag that corresponds to a C struct name."""
|
|
for tag, entry in STRUCT_NAME_MAP.items():
|
|
if len(entry) >= 4:
|
|
# Some entries have alternative names
|
|
if entry[0] == c_name or entry[3] == c_name:
|
|
return tag
|
|
elif entry[0] == c_name:
|
|
return tag
|
|
return None
|
|
|
|
|
|
_STRUCT_CACHE: dict = {}
|
|
|
|
|
|
def _compute_ast_struct_size(record_decl: dict, typedef_map: dict, ast: dict) -> int:
|
|
"""Compute total size of a struct from its Clang AST RecordDecl."""
|
|
total = 0
|
|
for child in record_decl.get('inner', []):
|
|
if child.get('kind') == 'FieldDecl':
|
|
qtype = child.get('type', {}).get('qualType', '')
|
|
if qtype.startswith('struct ') and '*' not in qtype and '(' not in qtype:
|
|
struct_name = qtype[len('struct '):].strip()
|
|
emb_record = _find_record_decl(ast, struct_name)
|
|
if emb_record and any(c.get('kind') == 'FieldDecl' for c in emb_record.get('inner', [])):
|
|
sz = _compute_ast_struct_size(emb_record, typedef_map, ast)
|
|
else:
|
|
sz = 0
|
|
else:
|
|
_, sz = _c_field_type_to_ndk(qtype, typedef_map)
|
|
total = _align_offset(total, sz) + sz
|
|
# Tail padding to 2
|
|
if total % 2 != 0:
|
|
total += 1
|
|
return total
|
|
|
|
|
|
def _add_simple_field(fields_raw: list, field_name: str, qual_type: str,
|
|
typedef_map: dict, ast: dict, used_names: set,
|
|
tag: str, prefix: str, offset: int) -> int:
|
|
"""Add a regular (non-struct) field to fields_raw, update offset."""
|
|
# Handle array types
|
|
array_match = re.match(r'(\w+(?:\s*\*)?)\s*\[(\d+)\]', qual_type)
|
|
count = 1
|
|
if array_match:
|
|
base_type_str = array_match.group(1)
|
|
count = int(array_match.group(2))
|
|
ndk_type, field_size = _c_field_type_to_ndk(base_type_str, typedef_map)
|
|
field_size *= count
|
|
else:
|
|
ndk_type, field_size = _c_field_type_to_ndk(qual_type, typedef_map)
|
|
|
|
# Compute aligned offset
|
|
offset = _align_offset(offset, field_size)
|
|
|
|
# Determine field name
|
|
raw_name = _c_field_name_to_asm(field_name, prefix)
|
|
base_name = raw_name
|
|
|
|
# Resolve name conflicts by prefixing with C struct name
|
|
if raw_name in used_names:
|
|
cname = STRUCT_NAME_MAP.get(tag, ('',))[0]
|
|
raw_name = cname + '_' + raw_name
|
|
|
|
used_names.add(base_name)
|
|
used_names.add(raw_name)
|
|
|
|
field_dict = {
|
|
'type': ndk_type,
|
|
'name': raw_name,
|
|
'extra_names': [],
|
|
'comment': '',
|
|
'_offset': offset,
|
|
'_size': field_size,
|
|
'_clang_processed': True,
|
|
'_is_array': bool(array_match and count > 1),
|
|
'_qual_type': qual_type,
|
|
}
|
|
if ndk_type == 'STRUCT':
|
|
field_dict['size_str'] = str(field_size)
|
|
fields_raw.append(field_dict)
|
|
return offset + field_size
|
|
|
|
|
|
def _inline_struct_fields(tag: str, ast: dict, typedef_map: dict,
|
|
used_names: set, offset: int) -> tuple[list, int]:
|
|
"""Recursively inline fields of a struct, handling embedded structs.
|
|
|
|
Returns (fields_raw, new_offset).
|
|
"""
|
|
entry = STRUCT_NAME_MAP.get(tag)
|
|
if not entry:
|
|
return [], offset
|
|
|
|
c_name = entry[0]
|
|
prefix = entry[1]
|
|
record = _find_record_decl(ast, c_name)
|
|
if record is None:
|
|
return [], offset
|
|
|
|
fields_raw = []
|
|
current_offset = offset
|
|
pending_anon_record = None # anonymous RecordDecl awaiting its FieldDecl
|
|
|
|
def _has_prefix(fname: str) -> bool:
|
|
"""Check if a field name starts with the struct's prefix (lowercase).
|
|
E.g. mp_Node starts with mp_ prefix, ViewPort does NOT start with sc_ prefix."""
|
|
cp = prefix.lower().rstrip('_')
|
|
return cp and fname.lower().startswith(cp)
|
|
|
|
# Iterate over all children; handle FieldDecl and anonymous RecordDecl (inline structs)
|
|
for child in record.get('inner', []):
|
|
kind = child.get('kind')
|
|
|
|
if kind == 'RecordDecl' and not child.get('name'):
|
|
# Anonymous inline struct: store for the following FieldDecl
|
|
pending_anon_record = child
|
|
continue
|
|
|
|
if kind == 'FieldDecl':
|
|
field_name = child.get('name', '')
|
|
qual_type = child.get('type', {}).get('qualType', '')
|
|
if not field_name or not qual_type:
|
|
continue
|
|
|
|
# Handle embedded structs/unions (not pointers) — including anonymous ones
|
|
is_anon_struct = qual_type.startswith('struct (unnamed') or qual_type.startswith('union (unnamed')
|
|
is_named_struct = (qual_type.startswith('struct ') or qual_type.startswith('union ')) and '*' not in qual_type
|
|
if is_named_struct or is_anon_struct:
|
|
if is_anon_struct and pending_anon_record:
|
|
# Inline the anonymous struct's fields (X, Y) directly into parent
|
|
for anon_child in pending_anon_record.get('inner', []):
|
|
if anon_child.get('kind') == 'FieldDecl':
|
|
fname = anon_child.get('name', '')
|
|
qtype = anon_child.get('type', {}).get('qualType', '')
|
|
if not fname or not qtype:
|
|
continue
|
|
current_offset = _add_simple_field(
|
|
fields_raw, fname, qtype, typedef_map, ast,
|
|
used_names, tag, prefix, current_offset)
|
|
pending_anon_record = None
|
|
continue
|
|
if is_named_struct and '(' not in qual_type:
|
|
struct_name = qual_type[len('struct '):].strip()
|
|
emb_tag = _find_tag_by_c_name(struct_name)
|
|
if emb_tag and _has_prefix(field_name):
|
|
emb_fields, current_offset = _inline_struct_fields(
|
|
emb_tag, ast, typedef_map, used_names, current_offset)
|
|
fields_raw.extend(emb_fields)
|
|
continue
|
|
|
|
# Anonymous struct or non-prefix embedded: compute size and create opaque block
|
|
if is_anon_struct:
|
|
# Find preceding anonymous RecordDecl
|
|
anon_record = None
|
|
for sib in record.get('inner', []):
|
|
if sib.get('kind') == 'RecordDecl' and not sib.get('name'):
|
|
anon_record = sib
|
|
break
|
|
if anon_record and any(c.get('kind') == 'FieldDecl' for c in anon_record.get('inner', [])):
|
|
struct_size = _compute_ast_struct_size(anon_record, typedef_map, ast)
|
|
else:
|
|
struct_size = 0
|
|
else:
|
|
emb_record = _find_record_decl(ast, struct_name)
|
|
if emb_record and any(c.get('kind') == 'FieldDecl' for c in emb_record.get('inner', [])):
|
|
struct_size = _compute_ast_struct_size(emb_record, typedef_map, ast)
|
|
else:
|
|
struct_size = 0
|
|
|
|
# Create opaque ubyte[N] field for the embedded struct
|
|
current_offset = _align_offset(current_offset, struct_size)
|
|
raw_name = _c_field_name_to_asm(field_name, prefix)
|
|
base_name = raw_name
|
|
if raw_name in used_names:
|
|
cname = STRUCT_NAME_MAP.get(tag, ('',))[0]
|
|
raw_name = cname + '_' + raw_name
|
|
used_names.add(base_name)
|
|
used_names.add(raw_name)
|
|
fields_raw.append({
|
|
'type': 'STRUCT',
|
|
'name': raw_name,
|
|
'extra_names': [],
|
|
'comment': '',
|
|
'_offset': current_offset,
|
|
'_size': struct_size,
|
|
'_clang_processed': True,
|
|
'size_str': str(struct_size),
|
|
})
|
|
current_offset += struct_size
|
|
continue
|
|
|
|
# Regular field (not struct or union)
|
|
current_offset = _add_simple_field(fields_raw, field_name, qual_type, typedef_map, ast, used_names, tag, prefix, current_offset)
|
|
|
|
return fields_raw, current_offset
|
|
|
|
|
|
# Which C headers to include per library for the clang AST dump
|
|
LIB_HEADERS = {
|
|
'exec': [],
|
|
'dos': ['dos/dosextens.h', 'dos/exall.h'],
|
|
'graphics': ['graphics/gfx.h', 'graphics/rastport.h', 'graphics/clip.h',
|
|
'graphics/view.h', 'graphics/text.h', 'graphics/layers.h',
|
|
'graphics/videocontrol.h'],
|
|
'intuition': ['intuition/intuition.h',
|
|
'graphics/gfx.h', 'graphics/view.h', 'graphics/rastport.h',
|
|
'graphics/clip.h', 'graphics/text.h', 'graphics/layers.h'],
|
|
'utility': ['utility/tagitem.h', 'utility/hooks.h', 'utility/date.h'],
|
|
'arexx': ['rexx/storage.h', 'rexx/rexxio.h', 'dos/dos.h'],
|
|
'iffparse': ['libraries/iffparse.h'],
|
|
'lowlevel': ['libraries/lowlevel.h', 'devices/timer.h', 'utility/tagitem.h'],
|
|
}
|
|
|
|
# Which struct tags belong to which library (to avoid cross-library duplication)
|
|
LIB_STRUCT_TAGS = {
|
|
'exec': {'LN', 'MLN', 'LH', 'MLH', 'MP', 'MN', 'IO', 'IS', 'LIB', 'TC_Struct'},
|
|
'dos': {'FileHandle', 'FileLock', 'Process', 'DosPacket', 'StandardPacket',
|
|
'DateStamp', 'FileInfoBlock', 'InfoData', 'ErrorString', 'ExAllData', 'ExAllControl'},
|
|
'graphics': {'RastPort', 'Layer', 'ClipRect', 'BitMap', 'View', 'TextFont',
|
|
'TextAttr', 'ColorMap', 'TmpRas', 'AreaInfo', 'TextExtent'},
|
|
'intuition': {'Window', 'Screen', 'IntuiMessage', 'Gadget', 'Image', 'Border',
|
|
'IntuiText', 'MenuItem', 'Requester', 'NewScreen', 'NewWindow',
|
|
'IBox', 'EasyStruct', 'ExtGadget', 'ExtIntuiMessage', 'DrawInfo',
|
|
'ColorSpec', 'Menu'},
|
|
'utility': {'TagItem', 'HOOK', 'CLOCKDATA'},
|
|
'arexx': {'RexxMsg'},
|
|
'iffparse': {'IFFHandle', 'IFFStreamCmd', 'ContextNode',
|
|
'StoredProperty', 'CollectionItem'},
|
|
'lowlevel': {'KeyQuery'},
|
|
'icon': {'DiskObject'},
|
|
}
|
|
|
|
# Additional .i files holding a library's structs/consts that live outside its
|
|
# own Include_I/<lib> directory: lib -> (subdir, filename)
|
|
LIB_EXTRA_I_FILES = {
|
|
'iffparse': ('libraries', 'iffparse.i'),
|
|
'icon': ('workbench', 'workbench.i'),
|
|
'lowlevel': ('libraries', 'lowlevel.i'),
|
|
}
|
|
|
|
|
|
def _lib_extra_i_files(ndk: str, lib: str) -> list:
|
|
"""Absolute paths of the extra .i files to scan for a library."""
|
|
entry = LIB_EXTRA_I_FILES.get(lib)
|
|
if not entry:
|
|
return []
|
|
path = os.path.join(ndk, 'Include_I', entry[0], entry[1])
|
|
return [path] if os.path.isfile(path) else []
|
|
|
|
|
|
def _library_struct_tags(lib_name: str) -> set:
|
|
"""Struct tags to emit for a library, including split base structs (e.g. IO_BASE)."""
|
|
tags = set(LIB_STRUCT_TAGS.get(lib_name, set()))
|
|
tags.update(f'{t}_BASE' for t in list(tags))
|
|
return tags
|
|
|
|
|
|
def parse_structs_from_clang(ndk_path: str, lib_name: str) -> dict:
|
|
"""Parse struct definitions from C headers via clang JSON AST dump.
|
|
|
|
Returns dict in the same raw_structs format as parse_struct_i_files().
|
|
Falls back to .i file parsing if clang is unavailable or fails.
|
|
"""
|
|
inc_path = os.path.join(ndk_path, 'Include_H')
|
|
|
|
# Build the C source to feed to clang
|
|
c_lines = ['#include <exec/exec.h>']
|
|
|
|
for hdr in LIB_HEADERS.get(lib_name, []):
|
|
c_lines.append(f'#include <{hdr}>')
|
|
|
|
c_source = '\n'.join(c_lines)
|
|
|
|
# Write temp C file and run clang
|
|
try:
|
|
with tempfile.NamedTemporaryFile(suffix='.c', mode='w', delete=False) as f:
|
|
f.write(c_source)
|
|
c_file = f.name
|
|
|
|
result = subprocess.run(
|
|
['clang', '-Xclang', '-ast-dump=json', '-fsyntax-only',
|
|
f'-I{inc_path}', c_file],
|
|
capture_output=True, text=True, timeout=30)
|
|
|
|
if result.returncode != 0:
|
|
print(f"Warning: clang error: {result.stderr}", file=sys.stderr)
|
|
return None
|
|
|
|
ast = json.loads(result.stdout)
|
|
except (subprocess.TimeoutExpired, FileNotFoundError, json.JSONDecodeError,
|
|
PermissionError, OSError) as e:
|
|
print(f"Warning: clang AST dump failed: {e}", file=sys.stderr)
|
|
return None
|
|
finally:
|
|
try:
|
|
os.unlink(c_file)
|
|
except OSError:
|
|
pass
|
|
|
|
# Build typedef map
|
|
typedef_map = _build_typedef_map(ast)
|
|
|
|
# Parse all structs in STRUCT_NAME_MAP that exist in the AST
|
|
allowed_tags = _library_struct_tags(lib_name)
|
|
structs = {}
|
|
for tag in STRUCT_NAME_MAP:
|
|
# Only include structs that belong to this library
|
|
if tag not in allowed_tags:
|
|
continue
|
|
entry = STRUCT_NAME_MAP[tag]
|
|
c_name = entry[0]
|
|
prefix = entry[1]
|
|
|
|
record = _find_record_decl(ast, c_name)
|
|
if record is None:
|
|
continue
|
|
|
|
# Check struct size from AST if available
|
|
if record.get('type', {}).get('qualType'):
|
|
t = record.get('type', {})
|
|
size_info = t.get('size', 0)
|
|
# Not all AST versions provide size in RecordDecl
|
|
used_names = set()
|
|
fields_raw, total_size = _inline_struct_fields(tag, ast, typedef_map,
|
|
used_names, 0)
|
|
|
|
# Tail padding: round total size to highest alignment
|
|
max_align = 2
|
|
if total_size % max_align != 0:
|
|
total_size += max_align - (total_size % max_align)
|
|
|
|
structs[tag] = {
|
|
'tag': tag,
|
|
'base_offset_str': '0',
|
|
'fields_raw': fields_raw,
|
|
'total_size': total_size,
|
|
'labels': [],
|
|
}
|
|
|
|
if not structs:
|
|
print("Warning: no structs found via clang AST dump", file=sys.stderr)
|
|
return None
|
|
|
|
return structs
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 3. REGISTRY + 4. TYPER - Global struct map and typed pointer resolution
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _build_global_struct_map(all_libs_raw: dict) -> dict:
|
|
"""Build C_Name -> {lib, prog8_name, qualified} for cross-lib typing.
|
|
all_libs_raw: dict lib -> raw_structs dict tag->raw
|
|
Returns dict C_Name -> {lib, prog8_name, tag, qualified_type}
|
|
"""
|
|
cmap = {}
|
|
for lib, raw_dict in all_libs_raw.items():
|
|
for tag, rs in raw_dict.items():
|
|
if tag.endswith('_BASE'):
|
|
continue
|
|
entry = STRUCT_NAME_MAP.get(tag)
|
|
if not entry:
|
|
continue
|
|
c_name = entry[0]
|
|
# lib is determined by LIB_STRUCT_TAGS: find owner lib for this tag
|
|
owner_lib = None
|
|
for olib, tags in LIB_STRUCT_TAGS.items():
|
|
if tag in tags:
|
|
owner_lib = olib
|
|
break
|
|
if owner_lib is None:
|
|
owner_lib = lib
|
|
# prog8 name is C name entry[0] (already Prog8 struct name)
|
|
prog8_name = c_name
|
|
cmap[c_name] = {
|
|
'lib': owner_lib,
|
|
'prog8_name': prog8_name,
|
|
'tag': tag,
|
|
}
|
|
# Also ensure all STRUCT_NAME_MAP entries that exist in any raw are included even if tag not in LIB_STRUCT_TAGS set for that lib's filtered view
|
|
for tag, entry in STRUCT_NAME_MAP.items():
|
|
if tag in cmap or tag.endswith('_BASE'):
|
|
continue
|
|
# check if any raw has this tag
|
|
for lib, raw_dict in all_libs_raw.items():
|
|
if tag in raw_dict:
|
|
c_name = entry[0]
|
|
if c_name not in cmap:
|
|
owner_lib = None
|
|
for olib, tags in LIB_STRUCT_TAGS.items():
|
|
if tag in tags:
|
|
owner_lib = olib
|
|
break
|
|
if owner_lib is None:
|
|
owner_lib = lib
|
|
cmap[c_name] = {'lib': owner_lib, 'prog8_name': c_name, 'tag': tag}
|
|
break
|
|
return cmap
|
|
|
|
|
|
def _qualified_prog8_pointer(c_name: str, current_lib: str, global_map: dict) -> str | None:
|
|
"""Return qualified typed pointer string for known struct C_Name, or None if unknown.
|
|
Returns e.g. '^^Node' if same lib, '^^exec.Node' if cross-lib.
|
|
"""
|
|
info = global_map.get(c_name)
|
|
if not info:
|
|
return None
|
|
owner = info['lib']
|
|
pname = info['prog8_name']
|
|
if owner == current_lib:
|
|
return f'^^{pname}'
|
|
else:
|
|
return f'^^{owner}.{pname}'
|
|
|
|
|
|
def _resolve_c_pointer_to_prog8(c_qual_type: str, typedef_map: dict, current_lib: str, global_map: dict) -> str | None:
|
|
"""Resolve a C qualType that is a pointer to a known struct to typed Prog8 pointer.
|
|
Returns typed string like '^^exec.Node' or None if not a typed struct pointer (fallback to generic).
|
|
"""
|
|
qt = c_qual_type.strip()
|
|
# Strip array suffix for pointer arrays: "struct Foo *[N]" -> "struct Foo *"
|
|
qt = re.sub(r'\s*\[.*\]\s*$', '', qt).strip()
|
|
# Strip const/volatile etc. (case-insensitive, handles CONST from SFD)
|
|
qt = re.sub(r'\b(const|volatile|register|unsigned|signed)\b', '', qt, flags=re.IGNORECASE).strip()
|
|
qt = re.sub(r'\s+', ' ', qt).strip()
|
|
# Must contain * to be pointer
|
|
if '*' not in qt and '(**' not in qt and '(*)(' not in qt:
|
|
return None
|
|
# Count stars: pointer-to-pointer stays generic
|
|
# Remove function pointer patterns early
|
|
if '(*' in qt or '(**' in qt:
|
|
return None
|
|
star_count = qt.count('*')
|
|
if star_count != 1:
|
|
return None
|
|
# Strip trailing * and spaces
|
|
base = qt.replace('*', '').strip()
|
|
# Remove leading struct/union
|
|
base = re.sub(r'^\s*(struct|union)\s+', '', base, flags=re.IGNORECASE).strip()
|
|
# Strip remaining qualifiers
|
|
base = re.sub(r'\b(const|volatile)\b', '', base, flags=re.IGNORECASE).strip()
|
|
base = re.sub(r'\s+', ' ', base).strip()
|
|
# Resolve typedef chain if needed
|
|
orig_base = base
|
|
seen = set()
|
|
while base in typedef_map and base not in seen:
|
|
seen.add(base)
|
|
resolved = typedef_map[base]
|
|
# If typedef resolves to something with *, try again
|
|
if '*' in resolved:
|
|
# e.g. typedef struct Node *NodePtr; base=NodePtr
|
|
qt2 = resolved
|
|
# Re-evaluate as pointer type
|
|
return _resolve_c_pointer_to_prog8(qt2, typedef_map, current_lib, global_map)
|
|
base = resolved.strip()
|
|
base = re.sub(r'^\s*(struct|union)\s+', '', base).strip()
|
|
base = re.sub(r'\b(const|volatile)\b', '', base).strip()
|
|
# Check string types: char * already handled but fallback
|
|
if base in ('char', 'STRPTR', 'TEXT', 'CONST_STRPTR', 'CONST_TEXT', 'BSTR'):
|
|
return None # caller will map to str
|
|
# Now base should be C struct name like Node, FileLock, etc.
|
|
# Try direct lookup
|
|
q = _qualified_prog8_pointer(base, current_lib, global_map)
|
|
if q is not None:
|
|
return q
|
|
# Try typedef base without struct prefix already handled
|
|
# Fallback generic
|
|
return None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers for driver - collection and generation
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _collect_raw_for_lib(ndk: str, lib: str) -> dict:
|
|
"""Collect raw struct dict for one lib (clang preferred, .i fallback)."""
|
|
arexx_alias = (lib == 'arexx')
|
|
inc = os.path.join(ndk, 'Include_I', 'rexx' if arexx_alias else lib)
|
|
exec_inc = os.path.join(ndk, 'Include_I', 'exec')
|
|
lib_i_files = _lib_extra_i_files(ndk, lib)
|
|
clang_structs = parse_structs_from_clang(ndk, lib)
|
|
if clang_structs is not None:
|
|
return clang_structs
|
|
all_raw = {}
|
|
if os.path.isdir(exec_inc):
|
|
all_raw.update(parse_struct_i_files(exec_inc))
|
|
if os.path.isdir(inc):
|
|
lib_structs = parse_struct_i_files(inc)
|
|
for tag in list(lib_structs.keys()):
|
|
if tag in all_raw:
|
|
print(f"Warning: struct '{tag}' from exec overwritten by {lib}", file=sys.stderr)
|
|
all_raw.update(lib_structs)
|
|
elif lib_i_files:
|
|
for fp in lib_i_files:
|
|
all_raw.update(_parse_one_i(fp))
|
|
_resolve_base_sizes(all_raw)
|
|
return all_raw
|
|
|
|
|
|
# Track current lib and global map for typer callbacks (used by field helpers)
|
|
_CURRENT_LIB_FOR_TYPER = None
|
|
_GLOBAL_MAP_FOR_TYPER = {}
|
|
_TYPEDEF_MAP_FOR_TYPER = {}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 1. DRIVER - Controls library set
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Convert Amiga NDK LVO, SFD, .i files to Prog8. Generates typed pointers cross-library.")
|
|
ap.add_argument('ndk_path', help="Path to AmigaNDK_headers directory")
|
|
ap.add_argument('output_dir', help="Path to amiga500 library output directory (e.g. compiler/res/prog8lib/amiga500)")
|
|
ap.add_argument('--shared-prefix', action='store_true',
|
|
help="prefix the output filenames with 'shared_amiga_' instead of naming them after the library")
|
|
args = ap.parse_args()
|
|
|
|
ndk = args.ndk_path.rstrip('/')
|
|
out_dir = args.output_dir.rstrip('/')
|
|
os.makedirs(out_dir, exist_ok=True)
|
|
name_prefix = 'shared_amiga_' if args.shared_prefix else ''
|
|
|
|
# Phase 1: Scan all libs for raw structs to build global registry
|
|
print(f"Scanning NDK at {ndk} for {len(AMIGA_LIBS)} libs...", file=sys.stderr)
|
|
all_libs_raw = {}
|
|
all_libs_typedef = {}
|
|
for lib in AMIGA_LIBS:
|
|
raw = _collect_raw_for_lib(ndk, lib)
|
|
all_libs_raw[lib] = raw
|
|
# Also collect typedef map for typer (from clang AST if available)
|
|
# parse_structs_from_clang already built typedef map internally, but we need to capture it
|
|
# For simplicity, build typedef map from clang AST separately if available
|
|
# Fallback: empty map
|
|
all_libs_typedef[lib] = {}
|
|
|
|
# For clang path, we need typedef maps per lib; re-parse to get them
|
|
# Instead, build global map from STRUCT_NAME_MAP entries that exist in any raw
|
|
global_map = _build_global_struct_map(all_libs_raw)
|
|
# Rename dos.DateStamp struct to avoid collision with extsub DateStamp in same block (ST cannot hold both with same scoped name)
|
|
if "DateStamp" in global_map and global_map["DateStamp"]["lib"] == "dos":
|
|
global_map["DateStamp"]["prog8_name"] = "DateStampStruct"
|
|
print(f"Global struct map: {len(global_map)} known structs", file=sys.stderr)
|
|
|
|
# Phase 2: Generate each lib with cross-lib typed pointers
|
|
for lib in AMIGA_LIBS:
|
|
arexx_alias = (lib == 'arexx')
|
|
lvo_lib = 'rexxsyslib' if arexx_alias else lib
|
|
lvo_p = os.path.join(ndk, 'Include_I', 'lvo', f'{lvo_lib}_lib.i')
|
|
sfd_p = os.path.join(ndk, 'SFD', f'{lvo_lib}_lib.sfd')
|
|
if not os.path.exists(lvo_p) or not os.path.exists(sfd_p):
|
|
print(f"Warning: LVO or SFD not found for '{lib}', skipping", file=sys.stderr)
|
|
continue
|
|
# Set typer globals for this lib before parsing SFD and generating structs
|
|
global _CURRENT_LIB_FOR_TYPER, _GLOBAL_MAP_FOR_TYPER, _TYPEDEF_MAP_FOR_TYPER
|
|
_CURRENT_LIB_FOR_TYPER = lib
|
|
_GLOBAL_MAP_FOR_TYPER = global_map
|
|
# Build typedef map for this lib from clang AST if available
|
|
# Try to get typedef map via re-parsing clang AST for this lib
|
|
try:
|
|
# Quick: build typedef map from clang AST if parse succeeds
|
|
from pathlib import Path
|
|
# Use existing parse_structs_from_clang path to also get typedef map
|
|
# We call a helper to get typedef map
|
|
_TYPEDEF_MAP_FOR_TYPER = {}
|
|
# Attempt to load clang AST typedefs: call parse_structs_from_clang with side effect capture
|
|
# Instead, directly build from AST if available
|
|
inc_path = os.path.join(ndk, 'Include_H')
|
|
c_lines = ['#include <exec/exec.h>']
|
|
for hdr in LIB_HEADERS.get(lib, []):
|
|
c_lines.append(f'#include <{hdr}>')
|
|
c_source = '\n'.join(c_lines)
|
|
import tempfile, subprocess, json
|
|
try:
|
|
with tempfile.NamedTemporaryFile(suffix='.c', mode='w', delete=False) as f:
|
|
f.write(c_source)
|
|
c_file = f.name
|
|
result = subprocess.run(['clang', '-Xclang', '-ast-dump=json', '-fsyntax-only', f'-I{inc_path}', c_file], capture_output=True, text=True, timeout=10)
|
|
if result.returncode == 0:
|
|
ast = json.loads(result.stdout)
|
|
_TYPEDEF_MAP_FOR_TYPER = _build_typedef_map(ast)
|
|
os.unlink(c_file)
|
|
except Exception:
|
|
pass
|
|
except Exception:
|
|
_TYPEDEF_MAP_FOR_TYPER = {}
|
|
|
|
lvos = parse_lvo(lvo_p)
|
|
# Need to re-parse SFD with typer globals set so parse_sfd_param can type extsubs
|
|
sfd_funcs = parse_sfd(sfd_p)
|
|
libname = sfd_funcs.pop('__libname', '')
|
|
bank = LIBRARY_BANK_MAP.get(lvo_lib) if arexx_alias else LIBRARY_BANK_MAP.get(lib)
|
|
btag = f"@bank {bank} " if bank else ""
|
|
|
|
all_raw = all_libs_raw.get(lib, {})
|
|
|
|
# Resolve structs with typer globals set
|
|
resolved = resolve_struct_sizes(all_raw)
|
|
allowed = _library_struct_tags(lib)
|
|
resolved = {tag: rs for tag, rs in resolved.items() if tag in allowed}
|
|
# Rename dos.DateStamp struct to avoid collision with extsub DateStamp (ST cannot hold both with same scoped name)
|
|
if lib == "dos" and "DateStamp" in resolved:
|
|
resolved["DateStamp"]["c_name"] = "DateStampStruct"
|
|
|
|
# Collect needed cross-lib imports from typed extsubs and structs
|
|
needed_imports = set()
|
|
# From extsubs
|
|
for info in sfd_funcs.values():
|
|
if not isinstance(info, dict):
|
|
continue
|
|
for pt, pn in info.get('params', []):
|
|
if pt.startswith('^^') and '.' in pt:
|
|
imp = pt[2:].split('.')[0]
|
|
if imp != lib:
|
|
needed_imports.add(imp)
|
|
rt = info.get('ret_type', '')
|
|
# ret_type is string like "struct Node *", need to check if typed would be cross-lib
|
|
# We can re-resolve return type
|
|
if rt and not info.get('is_void', True):
|
|
# Try to resolve return type as typed pointer
|
|
typed_ret = None
|
|
if _GLOBAL_MAP_FOR_TYPER and _CURRENT_LIB_FOR_TYPER:
|
|
typed_ret = _resolve_c_pointer_to_prog8(rt, _TYPEDEF_MAP_FOR_TYPER, lib, global_map)
|
|
if typed_ret and not _is_text_extent_pointer(typed_ret) and '.' in typed_ret:
|
|
imp = typed_ret[2:].split('.')[0]
|
|
if imp != lib:
|
|
needed_imports.add(imp)
|
|
# Also check if sfd_funcs already typed return is cross-lib (if stored as prog8_type)
|
|
# The sfd_funcs ret_type is still raw C, not yet converted; we already handled via typed_ret
|
|
# From structs
|
|
for rs in resolved.values():
|
|
for f in rs.get('fields', []):
|
|
ptype = f.get('prog8_type', '')
|
|
if ptype.startswith('^^') and '.' in ptype:
|
|
imp = ptype[2:].split('.')[0]
|
|
if imp != lib:
|
|
needed_imports.add(imp)
|
|
|
|
# Also check what the hand-written helpers reference (e.g. the
|
|
# openlib/closelib helpers call exec.OpenLibrary/CloseLibrary,
|
|
# the intuition helpers return ^^graphics. structs)
|
|
helper = LIBRARY_HELPERS.get(lib, '')
|
|
if helper and 'exec.' in helper:
|
|
needed_imports.add('exec')
|
|
if helper and 'graphics.' in helper:
|
|
needed_imports.add('graphics')
|
|
|
|
# Build output string
|
|
out_lines = []
|
|
out_lines.append(f";; Auto-generated from {lib}_lib.sfd and {lib}_lib.i")
|
|
if libname:
|
|
out_lines.append(f";; Library base: {libname} in prog8: sys.{libname[1:]}")
|
|
if bank:
|
|
out_lines.append(f";; Bank: {bank}")
|
|
out_lines.append(f";; Functions: {len(lvos)}")
|
|
out_lines.append("")
|
|
|
|
# Imports for cross-lib types
|
|
for imp in sorted(needed_imports):
|
|
out_lines.append(f"%import {imp}")
|
|
if needed_imports:
|
|
out_lines.append("")
|
|
|
|
out_lines.append(f"{lib} {{")
|
|
# The shared_amiga_*.p8 files are included by a thin <lib>.p8 wrapper
|
|
# that re-declares the same block, so they must merge instead of
|
|
# redefining it.
|
|
if name_prefix:
|
|
out_lines.append(f" %option merge, no_symbol_prefixing")
|
|
else:
|
|
out_lines.append(f" %option no_symbol_prefixing")
|
|
if helper:
|
|
out_lines.append(helper)
|
|
|
|
# extsub definitions (already typed via parse_sfd_param globals)
|
|
for name, lvo in sorted(lvos.items(), key=lambda x: x[1], reverse=True):
|
|
info = sfd_funcs.get(name)
|
|
pi = (info or {}).get('params', [])
|
|
regs = (info or {}).get('regs', [])
|
|
iv = (info or {}).get('is_void', True)
|
|
rt = (info or {}).get('ret_type')
|
|
pp = []
|
|
idx = 0
|
|
for r in regs:
|
|
if idx < len(pi):
|
|
pt, pn = pi[idx]
|
|
idx += 1
|
|
else:
|
|
pt, pn = 'long', f'reg_{r.lower()}'
|
|
pp.append(f"{pt} {pn} @{r.upper()}")
|
|
ps = ", ".join(pp)
|
|
rets = ''
|
|
if not iv:
|
|
# Typed return
|
|
pr = None
|
|
if _GLOBAL_MAP_FOR_TYPER and lib:
|
|
pr = _resolve_c_pointer_to_prog8(rt, _TYPEDEF_MAP_FOR_TYPER, lib, global_map)
|
|
if pr and _is_text_extent_pointer(pr):
|
|
pr = 'pointer'
|
|
if pr is None:
|
|
pr = parse_c_type(rt)[0] or 'long'
|
|
# BOOL returns should be bool
|
|
if rt and 'BOOL' in rt:
|
|
pr = 'bool'
|
|
rets = f" -> {pr} @D0"
|
|
out_lines.append(f" extsub {btag} {lvo} = {name}({ps}){rets}")
|
|
|
|
# structs (max_size 32768 for m68k, not 256)
|
|
structs_text = generate_structs(resolved, lib, indent=' ', header_text='struct definitions', max_size=32768)
|
|
if structs_text:
|
|
out_lines.append(structs_text)
|
|
|
|
# constants
|
|
inc = os.path.join(ndk, 'Include_I', 'rexx' if arexx_alias else lib)
|
|
lib_i_files = _lib_extra_i_files(ndk, lib)
|
|
consts = []
|
|
extra_files = []
|
|
if lib == 'intuition':
|
|
ie_file = os.path.join(ndk, 'Include_I', 'devices', 'inputevent.i')
|
|
if os.path.isfile(ie_file):
|
|
extra_files.append(ie_file)
|
|
if os.path.isdir(inc):
|
|
consts.extend(parse_consts_i_files(inc, extra_files=extra_files))
|
|
elif lib_i_files:
|
|
consts.extend(parse_consts_i_files('', extra_files=extra_files + lib_i_files))
|
|
consts_text = generate_consts(consts, lib, indent=' ', header_text='constants')
|
|
if consts_text:
|
|
out_lines.append(consts_text)
|
|
|
|
out_lines.append(f"}}")
|
|
out_lines.append(f";; End of auto-generated {lib}_lib.sfd")
|
|
content = "\n".join(out_lines) + "\n"
|
|
|
|
# Write transactionally only if changed
|
|
out_path = os.path.join(out_dir, f"{name_prefix}{lib}.p8")
|
|
existing = None
|
|
if os.path.exists(out_path):
|
|
with open(out_path, 'r', encoding='utf-8') as f:
|
|
existing = f.read()
|
|
if existing != content:
|
|
with open(out_path, 'w', encoding='utf-8') as f:
|
|
f.write(content)
|
|
print(f"Wrote {out_path}", file=sys.stderr)
|
|
else:
|
|
print(f"Unchanged {out_path}", file=sys.stderr)
|
|
|
|
# Reset globals
|
|
_CURRENT_LIB_FOR_TYPER = None
|
|
_GLOBAL_MAP_FOR_TYPER = {}
|
|
_TYPEDEF_MAP_FOR_TYPER = {}
|
|
|
|
|
|
def _tags_in_dir(d: str) -> set:
|
|
tags = set()
|
|
for fn in os.listdir(d):
|
|
if not fn.endswith('.i'):
|
|
continue
|
|
with open(os.path.join(d, fn), encoding='latin-1') as f:
|
|
for line in f:
|
|
m = re.match(r'STRUCTURE\s+(\w+)\s*,', line)
|
|
if m:
|
|
tags.add(m.group(1))
|
|
return tags
|
|
|
|
|
|
def print_mapping(cl):
|
|
print(file=sys.stderr)
|
|
for lib, bn in sorted(LIBRARY_BANK_MAP.items(), key=lambda x: x[1]):
|
|
m = " <--" if lib == cl else ""
|
|
print(f" bank {bn:3d} = {lib}{m}", file=sys.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|