#!/usr/bin/env python3
'''
KLL Expression Container
'''
# Copyright (C) 2016 by Jacob Alexander
#
# This file is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This file is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this file. If not, see .
### Imports ###
import copy
from common.id import CapId
### Decorators ###
## Print Decorator Variables
ERROR = '\033[5;1;31mERROR\033[0m:'
WARNING = '\033[5;1;33mWARNING\033[0m:'
### Classes ###
class Expression:
'''
Container class for KLL expressions
'''
def __init__( self, lparam, operator, rparam, context ):
'''
Initialize expression container
@param lparam: LOperatorData token
@param operator: Operator token
@param rparam: ROperatorData token
@param context: Parent context of expression
'''
# First stage/init
self.lparam_token = lparam
self.operator_token = operator
self.rparam_token = rparam
self.context = context # TODO, set multiple contexts for later stages
# Second stage
self.lparam_sub_tokens = []
self.rparam_sub_tokens = []
# Mutate class into the desired type
self.__class__ = {
'=>' : NameAssociationExpression,
'<=' : DataAssociationExpression,
'=' : AssignmentExpression,
':' : MapExpression,
}[ self.operator_type() ]
def operator_type( self ):
'''
Determine which base operator this operator is of
All : (map) expressions are tokenized/parsed the same way
@return Base string representation of the operator
'''
if ':' in self.operator_token.value:
return ':'
return self.operator_token.value
def final_tokens( self, no_filter=False ):
'''
Return the final list of tokens, must complete the second stage first
@param no_filter: If true, do not filter out Space tokens
@return Finalized list of tokens
'''
ret = self.lparam_sub_tokens + [ self.operator_token ] + self.rparam_sub_tokens
if not no_filter:
ret = [ x for x in ret if x.type != 'Space' ]
return ret
def regen_str( self ):
'''
Re-construct the string based off the original set of tokens
;
'''
return "{0}{1}{2};".format(
self.lparam_token.value,
self.operator_token.value,
self.rparam_token.value,
)
def point_chars( self, pos_list ):
'''
Using the regenerated string, point to a given list of characters
Used to indicate where a possible issue/syntax error is
@param pos_list: List of character indices
i.e.
> U"A" : : U"1";
> ^
'''
out = "\t{0}\n\t".format( self.regen_str() )
# Place a ^ character at the given locations
curpos = 1
for pos in sorted( pos_list ):
# Pad spaces, then add a ^
out += ' ' * (pos - curpos)
out += '^'
curpos += pos
return out
def rparam_start( self ):
'''
Starting positing char of rparam_token in a regen_str
'''
return len( self.lparam_token.value ) + len( self.operator_token.value )
def __repr__( self ):
# Build string representation based off of what has been set
# lparam, operator and rparam are always set
out = "Expression: {0}{1}{2}".format(
self.lparam_token.value,
self.operator_token.value,
self.rparam_token.value,
)
# TODO - Add more depending on what has been set
return out
def unique_keys( self ):
'''
Generates a list of unique identifiers for the expression that is mergeable
with other functional equivalent expressions.
This method should never get called directly as a generic Expression
'''
return [ ('UNKNOWN KEY', 'UNKNOWN EXPRESSION') ]
class AssignmentExpression( Expression ):
'''
Container class for assignment KLL expressions
'''
type = None
name = None
pos = None
value = None
## Setters ##
def array( self, name, pos, value ):
'''
Assign array assignment parameters to expression
@param name: Name of variable
@param pos: Array position of the value (if None, overwrite the entire array)
@param value: Value of the array, if pos is specified, this is the value of an element
@return: True if parsing was successful
'''
self.type = 'Array'
self.name = name
self.pos = pos
self.value = value
# If pos is not none, flatten
if pos is not None:
self.value = "".join( str( x ) for x in self.value )
return True
def merge_array( self, new_expression=None ):
'''
Merge arrays, used for position assignments
Merges unconditionally, make sure this is what you want to do first
If no additional array is specified, just "cap-off" array.
This does a proper array expansion into a python list.
@param new_expression: AssignmentExpression type array, ignore if None
'''
# First, check if base expression needs to be capped
if self.pos is not None:
# Generate a new string array
new_value = [""] * self.pos
# Append the old contents to the list
new_value.append( self.value )
self.value = new_value
# Clear pos, to indicate that array has been capped
self.pos = None
# Next, if a new_expression has been specified, merge in
if new_expression is not None and new_expression.pos is not None:
# Check if we need to extend the list
new_size = new_expression.pos + 1 - len( self.value )
if new_size > 0:
self.value.extend( [""] * new_size )
# Assign value to array
self.value[ new_expression.pos ] = new_expression.value
def variable( self, name, value ):
'''
Assign variable assignment parameters to expression
@param name: Name of variable
@param value: Value of variable
@return: True if parsing was successful
'''
self.type = 'Variable'
self.name = name
self.value = value
# Flatten value, often a list of various token types
self.value = "".join( str( x ) for x in self.value )
return True
def __repr__( self ):
if self.type == 'Variable':
return "{0} = {1};".format( self.name, self.value )
elif self.type == 'Array':
# Output KLL style array, double quoted elements, space-separated
if isinstance( self.value, list ):
output = "{0}[] =".format( self.name )
for value in self.value:
output += ' "{0}"'.format( value )
output += ";"
return output
# Single array assignment
else:
return "{0}[{1}] = {2};".format( self.name, self.pos, self.value )
return "ASSIGNMENT UNKNOWN"
def unique_keys( self ):
'''
Generates a list of unique identifiers for the expression that is mergeable
with other functional equivalent expressions.
'''
return [ ( self.name, self ) ]
class NameAssociationExpression( Expression ):
'''
Container class for name association KLL expressions
'''
type = None
name = None
association = None
## Setters ##
def capability( self, name, association, parameters ):
'''
Assign a capability C function name association
@param name: Name of capability
@param association: Name of capability in target backend output
@return: True if parsing was successful
'''
self.type = 'Capability'
self.name = name
self.association = CapId( association, 'Definition', parameters )
return True
def define( self, name, association ):
'''
Assign a define C define name association
@param name: Name of variable
@param association: Name of association in target backend output
@return: True if parsing was successful
'''
self.type = 'Define'
self.name = name
self.association = association
return True
def __repr__( self ):
return "{0} <= {1};".format( self.name, self.association )
def unique_keys( self ):
'''
Generates a list of unique identifiers for the expression that is mergeable
with other functional equivalent expressions.
'''
return [ ( self.name, self ) ]
class DataAssociationExpression( Expression ):
'''
Container class for data association KLL expressions
'''
type = None
association = None
value = None
## Setters ##
def animation( self, animations, animation_modifiers ):
'''
Animation definition and configuration
@return: True if parsing was successful
'''
self.type = 'Animation'
self.association = animations
self.value = animation_modifiers
return True
def animationFrame( self, animation_frames, pixel_modifiers ):
'''
Pixel composition of an Animation Frame
@return: True if parsing was successful
'''
self.type = 'AnimationFrame'
self.association = animation_frames
self.value = pixel_modifiers
return True
def pixelPosition( self, pixels, position ):
'''
Pixel Positioning
@return: True if parsing was successful
'''
for pixel in pixels:
pixel.setPosition( position )
self.type = 'PixelPosition'
self.association = pixels
return True
def scanCodePosition( self, scancodes, position ):
'''
Scan Code to Position Mapping
Note: Accepts lists of scan codes
Alone this isn't useful, but you can assign rows and columns using ranges instead of individually
@return: True if parsing was successful
'''
for scancode in scancodes:
scancode.setPosition( position )
self.type = 'ScanCodePosition'
self.association = scancodes
return True
def __repr__( self ):
if self.type in ['PixelPosition', 'ScanCodePosition']:
output = ""
for index, association in enumerate( self.association ):
if index > 0:
output += "; "
output += "{0}".format( association )
return "{0};".format( output )
return "{0} <= {1};".format( self.association, self.value )
def unique_keys( self ):
'''
Generates a list of unique identifiers for the expression that is mergeable
with other functional equivalent expressions.
'''
keys = []
# Positions require a bit more introspection to get the unique keys
if self.type in ['PixelPosition', 'ScanCodePosition']:
for index, key in enumerate( self.association ):
uniq_expr = self
# If there is more than one key, copy the expression
# and remove the non-related variants
if len( self.association ) > 1:
uniq_expr = copy.copy( self )
# Isolate variant by index
uniq_expr.association = [ uniq_expr.association[ index ] ]
keys.append( ( "{0}".format( key.unique_key() ), uniq_expr ) )
# AnimationFrames are already list of keys
# TODO Reorder frame assignments to dedup function equivalent mappings
elif self.type in ['AnimationFrame']:
for index, key in enumerate( self.association ):
uniq_expr = self
# If there is more than one key, copy the expression
# and remove the non-related variants
if len( self.association ) > 1:
uniq_expr = copy.copy( self )
# Isolate variant by index
uniq_expr.association = [ uniq_expr.association[ index ] ]
keys.append( ( "{0}".format( key ), uniq_expr ) )
# Otherwise treat as a single element
else:
keys = [ ( "{0}".format( self.association ), self ) ]
# Remove any duplicate keys
# TODO Stat? Might be at neat report about how many duplicates were squashed
keys = list( set( keys ) )
return keys
class MapExpression( Expression ):
'''
Container class for KLL map expressions
'''
type = None
triggers = None
operator = None
results = None
animation = None
animation_frame = None
pixels = None
position = None
## Setters ##
def scanCode( self, triggers, operator, results ):
'''
Scan Code mapping
@param triggers: Sequence of combos of ranges of namedtuples
@param operator: Type of map operation
@param results: Sequence of combos of ranges of namedtuples
@return: True if parsing was successful
'''
self.type = 'ScanCode'
self.triggers = triggers
self.operator = operator
self.results = results
return True
def usbCode( self, triggers, operator, results ):
'''
USB Code mapping
@param triggers: Sequence of combos of ranges of namedtuples
@param operator: Type of map operation
@param results: Sequence of combos of ranges of namedtuples
@return: True if parsing was successful
'''
self.type = 'USBCode'
self.triggers = triggers
self.operator = operator
self.results = results
return True
def animationTrigger( self, animation, operator, results ):
'''
Animation Trigger mapping
@param animation: Animation trigger of result
@param operator: Type of map operation
@param results: Sequence of combos of ranges of namedtuples
@return: True if parsing was successful
'''
self.type = 'Animation'
self.animation = animation
self.triggers = animation
self.operator = operator
self.results = results
return True
def pixelChannels( self, pixelmap, trigger ):
'''
Pixel Channel Composition
@return: True if parsing was successful
'''
self.type = 'PixelChannel'
self.pixel = pixelmap
self.position = trigger
return True
def sequencesOfCombosOfIds( self, expression_param ):
'''
Prettified Sequence of Combos of Identifiers
@param expression_param: Trigger or Result parameter of an expression
Scan Code Example
[[[S10, S16], [S42]], [[S11, S16], [S42]]] -> (S10 + S16, S42)|(S11 + S16, S42)
'''
output = ""
# Sometimes during error cases, might be None
if expression_param is None:
return output
# Iterate over each trigger/result variants (expanded from ranges), each one is a sequence
for index, sequence in enumerate( expression_param ):
if index > 0:
output += "|"
output += "("
# Iterate over each combo (element of the sequence)
for index, combo in enumerate( sequence ):
if index > 0:
output += ", "
# Iterate over each trigger identifier
for index, identifier in enumerate( combo ):
if index > 0:
output += " + "
output += "{0}".format( identifier )
output += ")"
return output
def elems( self ):
'''
Return number of trigger and result elements
Useful for determining if this is a trigger macro (2+)
Should always return at least (1,1) unless it's an invalid calculation
@return: ( triggers, results )
'''
elems = [ 0, 0 ]
# XXX Needed?
if self.type == 'PixelChannel':
return tuple( elems )
# Iterate over each trigger variant (expanded from ranges), each one is a sequence
for sequence in self.triggers:
# Iterate over each combo (element of the sequence)
for combo in sequence:
# Just measure the size of the combo
elems[0] += len( combo )
# Iterate over each result variant (expanded from ranges), each one is a sequence
for sequence in self.results:
# Iterate over each combo (element of the sequence)
for combo in sequence:
# Just measure the size of the combo
elems[1] += len( combo )
return tuple( elems )
def trigger_str( self ):
'''
String version of the trigger
Used for sorting
'''
# Pixel Channel Mapping doesn't follow the same pattern
if self.type == 'PixelChannel':
return "{0}".format( self.pixel )
return "{0}".format(
self.sequencesOfCombosOfIds( self.triggers ),
)
def result_str( self ):
'''
String version of the result
Used for sorting
'''
# Pixel Channel Mapping doesn't follow the same pattern
if self.type == 'PixelChannel':
return "{0}".format( self.position )
return "{0}".format(
self.sequencesOfCombosOfIds( self.results ),
)
def __repr__( self ):
# Pixel Channel Mapping doesn't follow the same pattern
if self.type == 'PixelChannel':
return "{0} : {1};".format( self.pixel, self.position )
return "{0} {1} {2};".format(
self.sequencesOfCombosOfIds( self.triggers ),
self.operator,
self.sequencesOfCombosOfIds( self.results ),
)
def unique_keys( self ):
'''
Generates a list of unique identifiers for the expression that is mergeable
with other functional equivalent expressions.
TODO: This function should re-order combinations to generate the key.
The final generated combo will be in the original order.
'''
keys = []
# Pixel Channel only has key per mapping
if self.type == 'PixelChannel':
keys = [ ( "{0}".format( self.pixel ), self ) ]
# Split up each of the keys
else:
# Iterate over each trigger/result variants (expanded from ranges), each one is a sequence
for index, sequence in enumerate( self.triggers ):
key = ""
uniq_expr = self
# If there is more than one key, copy the expression
# and remove the non-related variants
if len( self.triggers ) > 1:
uniq_expr = copy.copy( self )
# Isolate variant by index
uniq_expr.triggers = [ uniq_expr.triggers[ index ] ]
# Iterate over each combo (element of the sequence)
for index, combo in enumerate( sequence ):
if index > 0:
key += ", "
# Iterate over each trigger identifier
for index, identifier in enumerate( combo ):
if index > 0:
key += " + "
key += "{0}".format( identifier )
# Add key to list
keys.append( ( key, uniq_expr ) )
# Remove any duplicate keys
# TODO Stat? Might be at neat report about how many duplicates were squashed
keys = list( set( keys ) )
return keys