Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Local generated logs
/test/cyml/logs/
2 changes: 1 addition & 1 deletion doc/user/language.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Complex Types

The following complex types are supported:

- ``list`` - Dynamic list container (e.g., ``list`` for int list, ``floatlist`` for float list)
- ``list`` - Dynamic list container (e.g., ``intlist`` for int list, ``floatlist`` for float list)
- ``array`` - Fixed-size array container (e.g., ``intarray``, ``floatarray``)
- ``tuple`` - Immutable sequence of elements
- ``dict`` - Dictionary/hash map (key-value pairs)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ universal = 1


[tool:pytest]
addopts = -rf
addopts = -vv -s --tb=long -rA

[aliases]
test=pytest
Expand Down
9 changes: 9 additions & 0 deletions src/pycropml/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from pycropml.cyml import transpile_file, transpile_package, transpile_component

from pycropml.transpiler.main import languages
from pycropml.transpiler.logger import configure_logging, get_logger


def main():
Expand Down Expand Up @@ -64,8 +65,13 @@ def main():
parser.add_option("-l", "--languages", dest="languages", action="append",
choices=languages,
help="Target languages : " + ','.join(languages))
parser.add_option("--log-level", dest="log_level", default="WARNING",
help="Logging level: DEBUG, INFO, WARNING, ERROR, CRITICAL (default: WARNING)")

(opts, args) = parser.parse_args()
configure_logging(opts.log_level)
cli_logger = get_logger('cli')
cli_logger.debug('CLI started with args=%s options=%s', args, opts)

sourcef = None
pyx_filename = None
Expand Down Expand Up @@ -125,12 +131,15 @@ def main():
return

for language in langs:
cli_logger.info('Transpiling file %s to %s', sourcef, language)
status = transpile_file(sourcef, language)
elif package:
for language in langs:
cli_logger.info('Transpiling package %s to %s', sourcef, language)
status = transpile_package(sourcef, language)
else:
for language in langs:
cli_logger.info('Transpiling component %s to %s', sourcef, language)
status = transpile_component(sourcef, newpackage, language)


Expand Down
106 changes: 94 additions & 12 deletions src/pycropml/transpiler/Parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from Cython.Compiler import Options
from path import Path
import sys
from pycropml.transpiler.errors import PseudoCythonParseError
from pycropml.transpiler.logger import get_logger


logger = get_logger('transpiler.parser')

options_defaults = dict(
show_version = 0,
Expand Down Expand Up @@ -43,6 +48,71 @@
class opt:
def __init__(self, **kwds):
self.__dict__.update(kwds)


def _extract_position(exception):
"""Try to extract (line, column, detail) from Cython parser exceptions."""
line = None
column = None
detail = None

if getattr(exception, 'args', None):
first = exception.args[0]
if isinstance(first, tuple) and len(first) >= 3:
try:
line = int(first[1])
column = int(first[2])
except Exception:
line = None
column = None

if len(exception.args) > 1 and exception.args[1]:
detail = str(exception.args[1])

if not detail:
text = str(exception).strip()
if text:
detail = text.splitlines()[-1]
else:
detail = repr(exception)

return line, column, detail


def _source_excerpt(source, line, radius=2):
"""Return numbered source excerpt around a target line."""
if not source:
return ""

lines = source.splitlines()
if not line or line < 1 or line > len(lines):
return "\n".join(["%4d: %s" % (i + 1, l) for i, l in enumerate(lines[:8])])

start = max(1, line - radius)
end = min(len(lines), line + radius)
out = []
for idx in range(start, end + 1):
marker = " >>" if idx == line else " "
out.append("%s %4d: %s" % (marker, idx, lines[idx - 1]))
return "\n".join(out)


def _build_parse_error_message(module, source, exception):
line, column, detail = _extract_position(exception)
location = "unknown location"
if line and column:
location = "line %s, column %s" % (line, column)
elif line:
location = "line %s" % line

label = module if isinstance(module, Path) else "inline source"
excerpt = _source_excerpt(source, line)

return (
"PyCrop2ML parse error in %s at %s.\n"
"Reason: %s\n"
"Source excerpt:\n%s"
) % (label, location, detail, excerpt)

def parser(module):

Expand Down Expand Up @@ -74,15 +144,27 @@ def parser(module):
Scanning.FileSourceDescriptor: Represents a code source. Only file sources for Cython code supported
"""
options = opt(**options_defaults)
if isinstance(module, Path):
context = Main.Context([os.path.dirname(module)], {}, cpp=False, language_level=2, options=options)
scope = context.find_submodule(module)
with open(module.encode('utf-8'), 'r') as f:
source = f.read()
source_desc = Scanning.FileSourceDescriptor(module, source)
tree = context.parse(source_desc, scope, pxd=None, full_module_name=module)
else:
from Cython.Compiler.TreeFragment import parse_from_strings
#if sys.version_info[0]<3: module = unicode(module)
tree = parse_from_strings("module",module)
return tree
source = None

try:
logger.debug('Starting parse for module type=%s', type(module).__name__)
if isinstance(module, Path):
context = Main.Context([os.path.dirname(module)], {}, cpp=False, language_level=2, options=options)
scope = context.find_submodule(module)
with open(module.encode('utf-8'), 'r') as f:
source = f.read()
source_desc = Scanning.FileSourceDescriptor(module, source)
tree = context.parse(source_desc, scope, pxd=None, full_module_name=module)
else:
from Cython.Compiler.TreeFragment import parse_from_strings
source = module
#if sys.version_info[0]<3: module = unicode(module)
tree = parse_from_strings("module", module)
logger.debug('Parse succeeded for module type=%s', type(module).__name__)
return tree
except PseudoCythonParseError:
raise
except Exception as exception:
message = _build_parse_error_message(module, source, exception)
logger.error('Parse failed: %s', message)
raise PseudoCythonParseError(message)
86 changes: 60 additions & 26 deletions src/pycropml/transpiler/ast_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,17 @@
from pycropml.transpiler.builtin_typed_api import *
from pycropml.transpiler.errors import PseudoCythonTypeCheckError, PseudoCythonNotTranslatableError, translation_error, type_check_error
from pycropml.transpiler.api_transform import FUNCTION_API,CONSTANT_API, METHOD_API, Standard
from pycropml.transpiler.logger import get_logger
from Cython.Compiler.StringEncoding import EncodedString
from pycropml.transpiler.helpers import *
import unyt as u
from six.moves import map
from six.moves import zip


logger = get_logger('transpiler.ast_transform')




def deepestRightLeafUtil(root, lvl, maxlvl, isRight):
Expand Down Expand Up @@ -225,7 +229,7 @@ def visit_singleassignmentnode(self, node, lhs, rhs, location):
name = lhs.name
e = self.type_env[name]
if e is None:
self.notdeclared(name, location[0])
self.notdeclared(name, location)
elif e:
if e in ("list", "dict", "tuple", "array", "intlist", "floatlist", "intarray", "floatarray"):
a = self._compatible_types(
Expand Down Expand Up @@ -428,10 +432,11 @@ def visit_namenode(self, node, location):

id_type = self.type_env[id]
if id_type is None:
# Pass the full source lines so error formatting can map line/column correctly.
raise type_check_error(
'%s is not defined' % id,
location, self.lines[:location[1]])
location, self.lines)

else:
z = {'type': 'local', 'name': id, 'pseudo_type': id_type, "lineno": location}
if id in self.inp_unit:
Expand Down Expand Up @@ -827,11 +832,18 @@ def visit_simplecallnode(self, node, function, coerced_self, args, arg_tuple, lo
arg_nodes = [arg if not isinstance(arg, ExprNodes.Node) else self.visit_node(arg) for arg in args]
meth = [d for m in list(self._fromimport.values()) for d in m]
if function.name not in meth and function.name not in FUNCTION_API["math"] :
raise PseudoCythonNotTranslatableError(
"Function '%s' is not defined in function '%s' at line %s. "
"This function is not a built-in function or has not been imported." %
(function.name, self.function_name, location[0])
from pycropml.transpiler.errors import format_code_block
_loc = (location[0], location[1]) if location else None
_block = format_code_block(self.lines, _loc) if _loc else ''
_msg = (
"Function '%s' is not defined in function '%s' at line %d, column %d.\n"
"This function is not a built-in function or has not been imported."
% (function.name, self.function_name, location[0], location[1])
)
if _block:
_msg += '\n' + _block
logger.error(_msg)
raise PseudoCythonNotTranslatableError(_msg)
else:
if self.retrieve_library(function.name) not in self._imports:
self._imports.append(
Expand Down Expand Up @@ -1205,8 +1217,8 @@ def visit_cargdeclnode(self, node, base_type, declarator, default, annotation, l
else:
name = declarator.name
if base_type.name is None:
self.notdeclared(name, location[0])
self.checktype(base_type.name)
self.notdeclared(name, location)
self.checktype(base_type.name, location)
typet = ["intlist","floatlist","booleanlist","datetime","datelist"]
typearray = ["intarray"]

Expand Down Expand Up @@ -1378,31 +1390,34 @@ def _enum_names(self):
result.add(k)
return result

def checktype(self,base):
def checktype(self, base, location=None):
typet = ["int", "float","bool","datetime","str","list","dict","tuple",
"intlist","floatlist","booleanlist","datelist","strlist","stringlist","struct",
"double", "doublelist", "doublearray","floatarray", "intarray", "array", "strarray", "datetimelist" ]
types = list(self.struct.keys())
enum_types = list(self._enum_names())
z = typet + types + enum_types
if base not in z:
# Create a helpful error message
from pycropml.transpiler.errors import format_code_block
basic_types = ["int", "float", "bool", "str", "datetime"]
collection_types = ["list", "dict", "tuple", "array", "intlist", "floatlist", "booleanlist", "strlist", "intarray", "floatarray"]
struct_types = types if types else []

error_msg = (
f"Type '{base}' is not supported by CyML. "
f"Supported types include:\n"
f" Basic types: {', '.join(basic_types)}\n"
f" Collection types: {', '.join(collection_types)}"
)


loc_str = " at line %d, column %d" % (location[0], location[1]) if location else ""
error_msg = "Type '%s' is not supported by CyML%s.\n" % (base, loc_str)
error_msg += "Supported types include:\n"
error_msg += " Basic types: %s\n" % ', '.join(basic_types)
error_msg += " Collection types: %s" % ', '.join(collection_types)
if struct_types:
error_msg += f"\n Defined structs: {', '.join(struct_types)}"
error_msg += "\n Defined structs: %s" % ', '.join(struct_types)
if enum_types:
error_msg += f"\n Defined enums: {', '.join(enum_types)}"

error_msg += "\n Defined enums: %s" % ', '.join(enum_types)
if location and getattr(self, 'lines', None):
code_block = format_code_block(self.lines, location)
if code_block:
error_msg += "\n" + code_block

logger.error(error_msg)
raise PseudoCythonTypeCheckError(error_msg)


Expand All @@ -1417,7 +1432,7 @@ def visit_cvardefnode(self, node, base_type, declarators, location):
# Regular simple type
actual_type_name = base_type.name

self.checktype(actual_type_name)
self.checktype(actual_type_name, location)
typet = ["intlist","floatlist","booleanlist","stringlist","strlist","datetime","datelist", "datetimelist"]
typearray = ["intarray","floatarray","booleanarray","stringarray", "strarray"]
is_enum_type = actual_type_name in self._enum_names()
Expand Down Expand Up @@ -1992,9 +2007,28 @@ def _confirm_comparable(self, o, l, r, location):
location, self.lines[location[0]],
suggestions='comparable types in pseudo-cython: %s' % ' '.join(COMPARABLE_TYPES))

def notdeclared(self, name, line):
raise PseudoCythonTypeCheckError("variable %s is not declared at line %s\n" % (name, line),
)
def notdeclared(self, name, location=None):
from pycropml.transpiler.errors import format_code_block
if location and len(location) >= 2:
line, col = location[0], location[1]
msg = "Variable '%s' is not declared at line %d, column %d." % (name, line, col)
elif location and len(location) == 1:
line = location[0]
msg = "Variable '%s' is not declared at line %d." % (name, line)
else:
msg = "Variable '%s' is not declared." % name

# Improve diagnostics for malformed declarations like def f(2):
if isinstance(name, (int, float)) or (isinstance(name, str) and name.isdigit()):
msg += " The declaration looks invalid (a literal appears where an identifier/type was expected)."

if location and getattr(self, 'lines', None):
code_block = format_code_block(self.lines, location)
if code_block:
msg += "\n" + code_block

logger.error(msg)
raise PseudoCythonTypeCheckError(msg)

def _compatible_types(self, from_, to, err, silent=False):
'''if from_[0] == "array":
Expand Down
Loading
Loading