codegen: Lambda improvements (#1476)

* Use #line directives in generated C++ code for lambdas

The #line directive in gcc is meant specifically for pieces of imported
code included in generated code, exactly what happens with lambdas in
the yaml files: https://gcc.gnu.org/onlinedocs/cpp/Line-Control.html

With this change, if I add the following at line 165 of kithen.yaml:
    - lambda: undefined_var == 5;

then "$ esphome kitchen.yaml compile" shows the following:

INFO Reading configuration kitchen.yaml...
INFO Generating C++ source...
INFO Compiling app...
INFO Running:  platformio run -d kitchen
<...>
Compiling .pioenvs/kitchen/src/main.cpp.o
kitchen.yaml: In lambda function:
kitchen.yaml:165:7: error: 'undefined_var' was not declared in this scope
*** [.pioenvs/kitchen/src/main.cpp.o] Error 1
== [FAILED] Took 2.37 seconds ==

* Silence gcc warning on multiline macros in lambdas

When the \ is used at the end of the C++ source in a lambda (line
continuation, often used in preprocessor macros), esphome will copy that
into main.cpp once as code and once as a // commment.  gcc will complain
about the multiline commment:

Compiling .pioenvs/kitchen/src/main.cpp.o
kitchen.yaml:640:3: warning: multi-line comment [-Wcomment]

Try to replace the \ with a "<cont>" for lack of a better idea.
This commit is contained in:
Andrew Zaborowski 2021-01-24 00:17:15 +01:00 committed by GitHub
parent 52c67d715b
commit c7c71089ce
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 48 additions and 10 deletions

View file

@ -17,9 +17,10 @@ from esphome.const import ALLOWED_NAME_CHARS, CONF_AVAILABILITY, CONF_COMMAND_TO
CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, \ CONF_HOUR, CONF_MINUTE, CONF_SECOND, CONF_VALUE, CONF_UPDATE_INTERVAL, CONF_TYPE_ID, \
CONF_TYPE, CONF_PACKAGES CONF_TYPE, CONF_PACKAGES
from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \ from esphome.core import CORE, HexInt, IPAddress, Lambda, TimePeriod, TimePeriodMicroseconds, \
TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes TimePeriodMilliseconds, TimePeriodSeconds, TimePeriodMinutes, DocumentLocation
from esphome.helpers import list_starts_with, add_class_to_obj from esphome.helpers import list_starts_with, add_class_to_obj
from esphome.voluptuous_schema import _Schema from esphome.voluptuous_schema import _Schema
from esphome.yaml_util import ESPHomeDataBase
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -982,7 +983,11 @@ LAMBDA_ENTITY_ID_PROG = re.compile(r'id\(\s*([a-zA-Z0-9_]+\.[.a-zA-Z0-9_]+)\s*\)
def lambda_(value): def lambda_(value):
"""Coerce this configuration option to a lambda.""" """Coerce this configuration option to a lambda."""
if not isinstance(value, Lambda): if not isinstance(value, Lambda):
value = Lambda(string_strict(value)) start_mark = None
if isinstance(value, ESPHomeDataBase) and value.esp_range is not None:
start_mark = DocumentLocation.copy(value.esp_range.start_mark)
start_mark.line += value.content_offset
value = Lambda(string_strict(value), start_mark)
entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value) entity_id_parts = re.split(LAMBDA_ENTITY_ID_PROG, value.value)
if len(entity_id_parts) != 1: if len(entity_id_parts) != 1:
entity_ids = ' '.join("'{}'".format(entity_id_parts[i]) entity_ids = ' '.join("'{}'".format(entity_id_parts[i])

View file

@ -227,7 +227,7 @@ LAMBDA_PROG = re.compile(r'id\(\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\)(\.?)')
class Lambda: class Lambda:
def __init__(self, value): def __init__(self, value, start_mark=None):
# pylint: disable=protected-access # pylint: disable=protected-access
if isinstance(value, Lambda): if isinstance(value, Lambda):
self._value = value._value self._value = value._value
@ -235,6 +235,7 @@ class Lambda:
self._value = value self._value = value
self._parts = None self._parts = None
self._requires_ids = None self._requires_ids = None
self._source_location = start_mark
# https://stackoverflow.com/a/241506/229052 # https://stackoverflow.com/a/241506/229052
def comment_remover(self, text): def comment_remover(self, text):
@ -277,6 +278,10 @@ class Lambda:
def __repr__(self): def __repr__(self):
return f'Lambda<{self.value}>' return f'Lambda<{self.value}>'
@property
def source_location(self):
return self._source_location
class ID: class ID:
def __init__(self, id, is_declaration=False, type=None, is_manual=None): def __init__(self, id, is_declaration=False, type=None, is_manual=None):
@ -334,9 +339,21 @@ class DocumentLocation:
mark.column mark.column
) )
@classmethod
def copy(cls, location):
return cls(
location.document,
location.line,
location.column
)
def __str__(self): def __str__(self):
return f'{self.document} {self.line}:{self.column}' return f'{self.document} {self.line}:{self.column}'
@property
def as_line_directive(self):
return f'#line {self.line + 1} "{self.document}"'
class DocumentRange: class DocumentRange:
def __init__(self, start_mark: DocumentLocation, end_mark: DocumentLocation): def __init__(self, start_mark: DocumentLocation, end_mark: DocumentLocation):

View file

@ -1,6 +1,7 @@
import abc import abc
import inspect import inspect
import math import math
import re
# pylint: disable=unused-import, wrong-import-order # pylint: disable=unused-import, wrong-import-order
from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence from typing import Any, Generator, List, Optional, Tuple, Type, Union, Sequence
@ -188,13 +189,14 @@ class ParameterListExpression(Expression):
class LambdaExpression(Expression): class LambdaExpression(Expression):
__slots__ = ("parts", "parameters", "capture", "return_type") __slots__ = ("parts", "parameters", "capture", "return_type", "source")
def __init__(self, parts, parameters, capture: str = '=', return_type=None): def __init__(self, parts, parameters, capture: str = '=', return_type=None, source=None):
self.parts = parts self.parts = parts
if not isinstance(parameters, ParameterListExpression): if not isinstance(parameters, ParameterListExpression):
parameters = ParameterListExpression(*parameters) parameters = ParameterListExpression(*parameters)
self.parameters = parameters self.parameters = parameters
self.source = source
self.capture = capture self.capture = capture
self.return_type = safe_exp(return_type) if return_type is not None else None self.return_type = safe_exp(return_type) if return_type is not None else None
@ -202,7 +204,10 @@ class LambdaExpression(Expression):
cpp = f'[{self.capture}]({self.parameters})' cpp = f'[{self.capture}]({self.parameters})'
if self.return_type is not None: if self.return_type is not None:
cpp += f' -> {self.return_type}' cpp += f' -> {self.return_type}'
cpp += f' {{\n{self.content}\n}}' cpp += ' {\n'
if self.source is not None:
cpp += f'{self.source.as_line_directive}\n'
cpp += f'{self.content}\n}}'
return indent_all_but_first_and_last(cpp) return indent_all_but_first_and_last(cpp)
@property @property
@ -360,7 +365,7 @@ class LineComment(Statement):
self.value = value self.value = value
def __str__(self): def __str__(self):
parts = self.value.split('\n') parts = re.sub(r'\\\s*\n', r'<cont>\n', self.value, re.MULTILINE).split('\n')
parts = [f'// {x}' for x in parts] parts = [f'// {x}' for x in parts]
return '\n'.join(parts) return '\n'.join(parts)
@ -555,7 +560,7 @@ def process_lambda(
else: else:
parts[i * 3 + 1] = var parts[i * 3 + 1] = var
parts[i * 3 + 2] = '' parts[i * 3 + 2] = ''
yield LambdaExpression(parts, parameters, capture, return_type) yield LambdaExpression(parts, parameters, capture, return_type, value.source_location)
def is_template(value): def is_template(value):

View file

@ -11,7 +11,8 @@ import yaml.constructor
from esphome import core from esphome import core
from esphome.config_helpers import read_config_file from esphome.config_helpers import read_config_file
from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, DocumentRange from esphome.core import EsphomeError, IPAddress, Lambda, MACAddress, TimePeriod, \
DocumentRange, DocumentLocation
from esphome.helpers import add_class_to_obj from esphome.helpers import add_class_to_obj
from esphome.util import OrderedDict, filter_yaml_files from esphome.util import OrderedDict, filter_yaml_files
@ -30,9 +31,16 @@ class ESPHomeDataBase:
def esp_range(self): def esp_range(self):
return getattr(self, '_esp_range', None) return getattr(self, '_esp_range', None)
@property
def content_offset(self):
return getattr(self, '_content_offset', 0)
def from_node(self, node): def from_node(self, node):
# pylint: disable=attribute-defined-outside-init # pylint: disable=attribute-defined-outside-init
self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark) self._esp_range = DocumentRange.from_marks(node.start_mark, node.end_mark)
if isinstance(node, yaml.ScalarNode):
if node.style is not None and node.style in '|>':
self._content_offset = 1
class ESPForceValue: class ESPForceValue:
@ -257,7 +265,10 @@ class ESPHomeLoader(yaml.SafeLoader): # pylint: disable=too-many-ancestors
@_add_data_ref @_add_data_ref
def construct_lambda(self, node): def construct_lambda(self, node):
return Lambda(str(node.value)) start_mark = DocumentLocation.from_mark(node.start_mark)
if node.style is not None and node.style in '|>':
start_mark.line += 1
return Lambda(str(node.value), start_mark)
@_add_data_ref @_add_data_ref
def construct_force(self, node): def construct_force(self, node):