1import os.path
2import re
3
4from c_common.clsutil import classonly
5from c_parser.info import (
6    KIND,
7    DeclID,
8    Declaration,
9    TypeDeclaration,
10    TypeDef,
11    Struct,
12    Member,
13    FIXED_TYPE,
14)
15from c_parser.match import (
16    is_type_decl,
17    is_pots,
18    is_funcptr,
19)
20from c_analyzer.match import (
21    is_system_type,
22    is_process_global,
23    is_fixed_type,
24    is_immutable,
25)
26import c_analyzer as _c_analyzer
27import c_analyzer.info as _info
28import c_analyzer.datafiles as _datafiles
29from . import _parser, REPO_ROOT
30
31
32_DATA_DIR = os.path.dirname(__file__)
33KNOWN_FILE = os.path.join(_DATA_DIR, 'known.tsv')
34IGNORED_FILE = os.path.join(_DATA_DIR, 'ignored.tsv')
35NEED_FIX_FILE = os.path.join(_DATA_DIR, 'globals-to-fix.tsv')
36KNOWN_IN_DOT_C = {
37    'struct _odictobject': False,
38    'PyTupleObject': False,
39    'struct _typeobject': False,
40    'struct _arena': True,  # ???
41    'struct _frame': False,
42    'struct _ts': True,  # ???
43    'struct PyCodeObject': False,
44    'struct _is': True,  # ???
45    'PyWideStringList': True,  # ???
46    # recursive
47    'struct _dictkeysobject': False,
48}
49# These are loaded from the respective .tsv files upon first use.
50_KNOWN = {
51    # {(file, ID) | ID => info | bool}
52    #'PyWideStringList': True,
53}
54#_KNOWN = {(Struct(None, typeid.partition(' ')[-1], None)
55#           if typeid.startswith('struct ')
56#           else TypeDef(None, typeid, None)
57#           ): ([], {'unsupported': None if supported else True})
58#          for typeid, supported in _KNOWN_IN_DOT_C.items()}
59_IGNORED = {
60    # {ID => reason}
61}
62
63KINDS = frozenset((*KIND.TYPES, KIND.VARIABLE))
64
65
66def read_known():
67    if not _KNOWN:
68        # Cache a copy the first time.
69        extracols = None  # XXX
70        #extracols = ['unsupported']
71        known = _datafiles.read_known(KNOWN_FILE, extracols, REPO_ROOT)
72        # For now we ignore known.values() (i.e. "extra").
73        types, _ = _datafiles.analyze_known(
74            known,
75            analyze_resolved=analyze_resolved,
76        )
77        _KNOWN.update(types)
78    return _KNOWN.copy()
79
80
81def write_known():
82    raise NotImplementedError
83    datafiles.write_known(decls, IGNORED_FILE, ['unsupported'], relroot=REPO_ROOT)
84
85
86def read_ignored():
87    if not _IGNORED:
88        _IGNORED.update(_datafiles.read_ignored(IGNORED_FILE, relroot=REPO_ROOT))
89        _IGNORED.update(_datafiles.read_ignored(NEED_FIX_FILE, relroot=REPO_ROOT))
90    return dict(_IGNORED)
91
92
93def write_ignored():
94    raise NotImplementedError
95    _datafiles.write_ignored(variables, IGNORED_FILE, relroot=REPO_ROOT)
96
97
98def analyze(filenames, *,
99            skip_objects=False,
100            **kwargs
101            ):
102    if skip_objects:
103        # XXX Set up a filter.
104        raise NotImplementedError
105
106    known = read_known()
107
108    decls = iter_decls(filenames)
109    results = _c_analyzer.analyze_decls(
110        decls,
111        known,
112        analyze_resolved=analyze_resolved,
113    )
114    analysis = Analysis.from_results(results)
115
116    return analysis
117
118
119def iter_decls(filenames, **kwargs):
120    decls = _c_analyzer.iter_decls(
121        filenames,
122        # We ignore functions (and statements).
123        kinds=KINDS,
124        parse_files=_parser.parse_files,
125        **kwargs
126    )
127    for decl in decls:
128        if not decl.data:
129            # Ignore forward declarations.
130            continue
131        yield decl
132
133
134def analyze_resolved(resolved, decl, types, knowntypes, extra=None):
135    if decl.kind not in KINDS:
136        # Skip it!
137        return None
138
139    typedeps = resolved
140    if typedeps is _info.UNKNOWN:
141        if decl.kind in (KIND.STRUCT, KIND.UNION):
142            typedeps = [typedeps] * len(decl.members)
143        else:
144            typedeps = [typedeps]
145    #assert isinstance(typedeps, (list, TypeDeclaration)), typedeps
146
147    if extra is None:
148        extra = {}
149    elif 'unsupported' in extra:
150        raise NotImplementedError((decl, extra))
151
152    unsupported = _check_unsupported(decl, typedeps, types, knowntypes)
153    extra['unsupported'] = unsupported
154
155    return typedeps, extra
156
157
158def _check_unsupported(decl, typedeps, types, knowntypes):
159    if typedeps is None:
160        raise NotImplementedError(decl)
161
162    if decl.kind in (KIND.STRUCT, KIND.UNION):
163        return _check_members(decl, typedeps, types, knowntypes)
164    elif decl.kind is KIND.ENUM:
165        if typedeps:
166            raise NotImplementedError((decl, typedeps))
167        return None
168    else:
169        return _check_typedep(decl, typedeps, types, knowntypes)
170
171
172def _check_members(decl, typedeps, types, knowntypes):
173    if isinstance(typedeps, TypeDeclaration):
174        raise NotImplementedError((decl, typedeps))
175
176    #members = decl.members or ()  # A forward decl has no members.
177    members = decl.members
178    if not members:
179        # A forward decl has no members, but that shouldn't surface here..
180        raise NotImplementedError(decl)
181    if len(members) != len(typedeps):
182        raise NotImplementedError((decl, typedeps))
183
184    unsupported = []
185    for member, typedecl in zip(members, typedeps):
186        checked = _check_typedep(member, typedecl, types, knowntypes)
187        unsupported.append(checked)
188    if any(None if v is FIXED_TYPE else v for v in unsupported):
189        return unsupported
190    elif FIXED_TYPE in unsupported:
191        return FIXED_TYPE
192    else:
193        return None
194
195
196def _check_typedep(decl, typedecl, types, knowntypes):
197    if not isinstance(typedecl, TypeDeclaration):
198        if hasattr(type(typedecl), '__len__'):
199            if len(typedecl) == 1:
200                typedecl, = typedecl
201    if typedecl is None:
202        # XXX Fail?
203        return 'typespec (missing)'
204    elif typedecl is _info.UNKNOWN:
205        # XXX Is this right?
206        return 'typespec (unknown)'
207    elif not isinstance(typedecl, TypeDeclaration):
208        raise NotImplementedError((decl, typedecl))
209
210    if isinstance(decl, Member):
211        return _check_vartype(decl, typedecl, types, knowntypes)
212    elif not isinstance(decl, Declaration):
213        raise NotImplementedError(decl)
214    elif decl.kind is KIND.TYPEDEF:
215        return _check_vartype(decl, typedecl, types, knowntypes)
216    elif decl.kind is KIND.VARIABLE:
217        if not is_process_global(decl):
218            return None
219        checked = _check_vartype(decl, typedecl, types, knowntypes)
220        return 'mutable' if checked is FIXED_TYPE else checked
221    else:
222        raise NotImplementedError(decl)
223
224
225def _check_vartype(decl, typedecl, types, knowntypes):
226    """Return failure reason."""
227    checked = _check_typespec(decl, typedecl, types, knowntypes)
228    if checked:
229        return checked
230    if is_immutable(decl.vartype):
231        return None
232    if is_fixed_type(decl.vartype):
233        return FIXED_TYPE
234    return 'mutable'
235
236
237def _check_typespec(decl, typedecl, types, knowntypes):
238    typespec = decl.vartype.typespec
239    if typedecl is not None:
240        found = types.get(typedecl)
241        if found is None:
242            found = knowntypes.get(typedecl)
243
244        if found is not None:
245            _, extra = found
246            if extra is None:
247                # XXX Under what circumstances does this happen?
248                extra = {}
249            unsupported = extra.get('unsupported')
250            if unsupported is FIXED_TYPE:
251                unsupported = None
252            return 'typespec' if unsupported else None
253    # Fall back to default known types.
254    if is_pots(typespec):
255        return None
256    elif is_system_type(typespec):
257        return None
258    elif is_funcptr(decl.vartype):
259        return None
260    return 'typespec'
261
262
263class Analyzed(_info.Analyzed):
264
265    @classonly
266    def is_target(cls, raw):
267        if not super().is_target(raw):
268            return False
269        if raw.kind not in KINDS:
270            return False
271        return True
272
273    #@classonly
274    #def _parse_raw_result(cls, result, extra):
275    #    typedecl, extra = super()._parse_raw_result(result, extra)
276    #    if typedecl is None:
277    #        return None, extra
278    #    raise NotImplementedError
279
280    def __init__(self, item, typedecl=None, *, unsupported=None, **extra):
281        if 'unsupported' in extra:
282            raise NotImplementedError((item, typedecl, unsupported, extra))
283        if not unsupported:
284            unsupported = None
285        elif isinstance(unsupported, (str, TypeDeclaration)):
286            unsupported = (unsupported,)
287        elif unsupported is not FIXED_TYPE:
288            unsupported = tuple(unsupported)
289        self.unsupported = unsupported
290        extra['unsupported'] = self.unsupported  # ...for __repr__(), etc.
291        if self.unsupported is None:
292            #self.supported = None
293            self.supported = True
294        elif self.unsupported is FIXED_TYPE:
295            if item.kind is KIND.VARIABLE:
296                raise NotImplementedError(item, typedecl, unsupported)
297            self.supported = True
298        else:
299            self.supported = not self.unsupported
300        super().__init__(item, typedecl, **extra)
301
302    def render(self, fmt='line', *, itemonly=False):
303        if fmt == 'raw':
304            yield repr(self)
305            return
306        rendered = super().render(fmt, itemonly=itemonly)
307        # XXX ???
308        #if itemonly:
309        #    yield from rendered
310        supported = self.supported
311        if fmt in ('line', 'brief'):
312            rendered, = rendered
313            parts = [
314                '+' if supported else '-' if supported is False else '',
315                rendered,
316            ]
317            yield '\t'.join(parts)
318        elif fmt == 'summary':
319            raise NotImplementedError(fmt)
320        elif fmt == 'full':
321            yield from rendered
322            if supported:
323                yield f'\tsupported:\t{supported}'
324        else:
325            raise NotImplementedError(fmt)
326
327
328class Analysis(_info.Analysis):
329    _item_class = Analyzed
330
331    @classonly
332    def build_item(cls, info, result=None):
333        if not isinstance(info, Declaration) or info.kind not in KINDS:
334            raise NotImplementedError((info, result))
335        return super().build_item(info, result)
336
337
338def check_globals(analysis):
339    # yield (data, failure)
340    ignored = read_ignored()
341    for item in analysis:
342        if item.kind != KIND.VARIABLE:
343            continue
344        if item.supported:
345            continue
346        if item.id in ignored:
347            continue
348        reason = item.unsupported
349        if not reason:
350            reason = '???'
351        elif not isinstance(reason, str):
352            if len(reason) == 1:
353                reason, = reason
354        reason = f'({reason})'
355        yield item, f'not supported {reason:20}\t{item.storage or ""} {item.vartype}'
356