from typing import NamedTuple
from functools import reduce
from .opcodes import Op, create_push, create_plain_op
from .utils import build_unique_dict, set_unique


ContextId = tuple[int, ...]
MarkId = tuple[ContextId, int]
Mark = NamedTuple('Mark', [('mid', MarkId)])
MarkRef = NamedTuple('MarkRef', [('mid', MarkId)])
MarkDeltaRef = NamedTuple('MarkDeltaRef', [('start', MarkId), ('end', MarkId)])
SizedRef = NamedTuple(
    'SizedRef',
    [('ref', MarkRef | MarkDeltaRef), ('offset_size', int)]
)


Asm = Op | Mark | MarkRef | MarkDeltaRef | bytes
SolidAsm = Op | Mark | SizedRef | bytes

START_SUB_ID = 0
END_SUB_ID = 1


def set_size(ref: SizedRef, size: int) -> SizedRef:
    return SizedRef(ref.ref, size)


def min_static_size(step: Asm) -> int:
    if isinstance(step, Op):
        return 1 + len(step.extra_data)
    elif isinstance(step, bytes):
        return len(step)
    elif isinstance(step, (MarkRef, MarkDeltaRef)):
        return 1
    elif isinstance(step, Mark):
        return 0
    else:
        raise TypeError(f'Unhandled step {step}')


def get_size(step: SolidAsm) -> int:
    if isinstance(step, Op):
        return 1 + len(step.extra_data)
    elif isinstance(step, bytes):
        return len(step)
    elif isinstance(step, SizedRef):
        return 1 + step.offset_size
    elif isinstance(step, Mark):
        return 0
    else:
        raise TypeError(f'Unhandled step {step}')


def needed_bytes(x: int) -> int:
    return max((x.bit_length() + 7) // 8, 1)


def get_min_static_size_bytes(asm: list[Asm]) -> int:
    '''Compute the minimum size in bytes that'd capture any code offset'''
    ref_count = sum(isinstance(step, (MarkRef, MarkDeltaRef)) for step in asm)
    min_static_total_size = sum(map(min_static_size, asm))
    ref_bytes = 1
    while ((1 << (8 * ref_bytes)) - 1) < min_static_total_size + ref_bytes * ref_count:
        ref_bytes += 1
    return ref_bytes


def asm_to_solid(asm: list[Asm]) -> list[SolidAsm]:
    size_bytes = get_min_static_size_bytes(asm)
    assert size_bytes <= 6
    return [
        SizedRef(step, size_bytes)
        if isinstance(step, (MarkRef, MarkDeltaRef))
        else step
        for step in asm
    ]


def validate_asm(asm: list[Asm]) -> None:
    '''
    Checks that MarkDeltaRef steps have strictly different, correctly ordered references and that all
    marks have unique IDs
    '''
    # Checks that all MarkIDs are unique while build indices dict
    indices: dict[MarkId, int] = build_unique_dict(
        (step.mid, i)
        for i, step in enumerate(asm)
        if isinstance(step, Mark)
    )
    for i, step in enumerate(asm):
        if isinstance(step, MarkRef):
            assert step.mid in indices, f'Assembly step #{i} has invalid reference to {step.mid}'
        elif isinstance(step, MarkDeltaRef):
            assert step.start in indices, f'Assembly step #{i} has invalid reference to {step.start}'
            assert step.end in indices, f'Assembly step #{i} has invalid reference to {step.end}'
            assert indices[step.end] > indices[step.start], \
                f'Assembly step #{i} references negative delta'


def get_solid_offsets(asm: list[SolidAsm]) -> dict[MarkId, int]:
    mark_offsets: dict[MarkId, int] = {}
    offset = 0
    for step in asm:
        if isinstance(step, Mark):
            mark_offsets[step.mid] = offset
        offset += get_size(step)
    return mark_offsets


def shorten_asm_once(asm: list[SolidAsm]) -> tuple[bool, list[SolidAsm]]:
    mark_offsets: dict[MarkId, int] = get_solid_offsets(asm)

    changed_any = False
    shortened_steps: list[SolidAsm] = []
    for step in asm:
        if isinstance(step, SizedRef):
            ref = step.ref
            if isinstance(ref, MarkRef):
                req_size = needed_bytes(mark_offsets[ref.mid])
                if req_size != step.offset_size:
                    changed_any = True
                    step = set_size(step, req_size)
            elif isinstance(ref, MarkDeltaRef):
                req_size = needed_bytes(
                    mark_offsets[ref.end] - mark_offsets[ref.start]
                )
                if req_size != step.offset_size:
                    changed_any = True
                    step = set_size(step, req_size)
            else:
                assert False  # Sanity check
        shortened_steps.append(step)

    return changed_any, shortened_steps


def shorten_asm(asm: list[SolidAsm]) -> list[SolidAsm]:
    changed_any = True
    while changed_any:
        changed_any, asm = shorten_asm_once(asm)
    return asm


def solid_asm_to_bytecode(asm: list[SolidAsm]) -> bytes:
    mark_offsets: dict[MarkId, int] = get_solid_offsets(asm)
    final_bytes: bytes = bytes()

    # Create skeleton for final bytecode
    for step in asm:
        if isinstance(step, Op):
            final_bytes += bytes(step.get_bytes())
        elif isinstance(step, bytes):
            final_bytes += step
        elif isinstance(step, SizedRef):
            ref = step.ref
            if isinstance(ref, MarkRef):
                value = mark_offsets[ref.mid]
            elif isinstance(ref, MarkDeltaRef):
                value = mark_offsets[ref.end] - mark_offsets[ref.start]
            else:
                assert False
            push = create_push(value.to_bytes(step.offset_size, 'big'))
            final_bytes += bytes(push.get_bytes())
        elif isinstance(step, Mark):
            # Mark generates no bytes
            pass
        else:
            raise ValueError(f'Unrecognized assembly step {step}')

    return final_bytes


def asm_to_bytecode(asm: list[Asm]) -> bytes:
    validate_asm(asm)
    solid_asm = asm_to_solid(asm)
    solid_asm = shorten_asm(solid_asm)
    return solid_asm_to_bytecode(solid_asm)


def minimal_deploy(runtime: bytes) -> bytes:
    start: MarkId = tuple(), START_SUB_ID
    end: MarkId = tuple(), END_SUB_ID
    # TODO: Add pre-shanghai (no PUSH0) toggle
    return asm_to_bytecode([
        MarkDeltaRef(start, end),
        create_plain_op('dup1'),
        MarkRef(start),
        create_plain_op('push0'),
        create_plain_op('codecopy'),
        create_plain_op('push0'),
        create_plain_op('return'),
        Mark(start),
        runtime,
        Mark(end)
    ])

py_huff/assembler.py
Philogy/py-huff
from argparse import ArgumentParser
from .compile import compile


def parse_args():
    parser = ArgumentParser(
        description='A CLI for compiling Huff source code files to bytecode'
    )
    parser.add_argument('path', type=str)
    parser.add_argument('--runtime', '-r', action='store_true')
    parser.add_argument('--deploy', '-b', action='store_true')
    return parser.parse_args()


def main() -> None:
    args = parse_args()

    compiled = compile(args.path)

    if args.runtime and args.deploy:
        print(f'bytecode: {compiled.deploy.hex()}')
        print(f'\nruntime: {compiled.runtime.hex()}')
    elif args.runtime:
        print(compiled.runtime.hex())
    elif args.deploy:
        print(compiled.deploy.hex())
    else:
        print('WARNING: Neither runtime or deploy bytecode output')


if __name__ == '__main__':
    main()

py_huff/cli.py
Philogy/py-huff
from typing import NamedTuple, Callable
from Crypto.Hash import keccak
from .lexer import ExNode
from .assembler import *
from .parser import *
from .opcodes import OP_MAP, Op


def keccak256(preimage: bytes) -> bytes:
    return keccak.new(data=preimage, digest_bits=256).digest()


MacroArg = Op | MarkRef
GlobalScope = NamedTuple(
    'GlobalScope',
    [
        ('macros', dict[Identifier, Macro]),
        ('constants', dict[Identifier, Op]),
        ('code_tables', dict[Identifier, CodeTable]),
        ('functions', dict[Identifier, ExNode]),
        ('events', dict[Identifier, ExNode])
    ]
)


def not_implemented(fn_name, *_) -> list[Asm]:
    return []
    raise ValueError(f'Built-in {fn_name} not implemented yet')


def table_start(name, g: GlobalScope, args: list[InvokeArg]) -> list[Asm]:
    assert len(args) == 1, f'{name} expects 1 argument, received {len(args)}'
    arg, = args
    assert isinstance(arg, GeneralRef), \
        f'{name} does not support argument {arg}'
    assert arg.ident in g.code_tables, f'Undefined table "{arg.ident}"'
    # TODO: Support more tables
    table = g.code_tables[arg.ident]
    mid: MarkId = (table.top_level_id,), START_SUB_ID

    return [MarkRef(mid)]


def table_size(name, g: GlobalScope, args: list[InvokeArg]) -> list[Asm]:
    assert len(args) == 1, f'{name} expects 1 argument, received {len(args)}'
    arg, = args
    assert isinstance(arg, GeneralRef), \
        f'{name} does not support argument {arg}'
    assert arg.ident in g.code_tables, f'Undefined table "{arg.ident}"'
    # TODO: Support more tables
    table = g.code_tables[arg.ident]
    start_mid: MarkId = (table.top_level_id,), START_SUB_ID
    end_mid: MarkId = (table.top_level_id,), END_SUB_ID

    return [MarkDeltaRef(start_mid, end_mid)]


def function_sig(name, g: GlobalScope, args: list[InvokeArg]) -> list[Asm]:
    assert len(args) == 1, f'{name} expects 1 argument, received {len(args)}'
    arg, = args
    assert isinstance(arg, GeneralRef), \
        f'{name} does not support argument {arg}'
    assert arg.ident in g.functions, f'Undefined function "{arg.ident}"'
    sig = function_to_sig(g.functions[arg.ident])
    return [
        create_push(keccak256(sig.encode())[:4])
    ]


def event_hash(name, g: GlobalScope, args: list[InvokeArg]) -> list[Asm]:
    assert len(args) == 1, f'{name} expects 1 argument, received {len(args)}'
    arg, = args
    assert isinstance(arg, GeneralRef), \
        f'{name} does not support argument {arg}'
    assert arg.ident in g.events, f'Undefined event "{arg.ident}"'
    sig = event_to_sig(g.events[arg.ident])
    return [
        create_push(keccak256(sig.encode()))
    ]


BUILT_INS: dict[str, Callable[[str, GlobalScope, list[InvokeArg]], list[Asm]]] = {
    '__EVENT_HASH': event_hash,
    '__FUNC_SIG': function_sig,
    '__codesize': not_implemented,
    '__tablestart': table_start,
    '__tablesize': table_size
}


def invoke_built_in(fn_name: str, g: GlobalScope, args: list[InvokeArg]) -> list[Asm]:
    return BUILT_INS[fn_name](fn_name, g, args)


def expand_macro_to_asm(
    macro_ident: Identifier,
    g: GlobalScope,
    args: list[MacroArg],
    labels: dict[Identifier, MarkId],
    ctx_id: ContextId,
    visited_macros: tuple[Identifier, ...]
) -> list[Asm]:
    assert macro_ident not in visited_macros, f'Circular macro refrence in {macro_ident}'
    assert macro_ident in g.macros, f'Unrecognized macro "{macro_ident}"'
    macro = g.macros[macro_ident]
    assert len(args) == len(macro.params), \
        f'macro "{macro_ident}" received {len(args)} args, expected {len(macro.params)}'
    visited_macros += (macro_ident,)

    ident_to_arg = {
        ident: arg
        for ident, arg in zip(macro.params, args)
    }
    if args is None:
        args = []

    asm: list[Asm] = []

    for i, label_def in enumerate(el for el in macro.body if isinstance(el, LabelDef)):
        label = label_def.ident
        dest_id: MarkId = ctx_id, i
        # TODO: Add warning when invoked macro has label shadowing parent
        assert label not in labels or labels[label] != dest_id, \
            f'Duplicate label "{label}" in macro "{macro.ident}"'
        labels[label] = dest_id

    def lookup_label(ident: Identifier) -> MarkRef:
        assert ident in labels, f'Label "{ident}" not found in scope ({ctx_id})'
        return MarkRef(labels[ident])

    def lookup_arg(ident: Identifier) -> MacroArg:
        assert ident in ident_to_arg, f'Invalid macro argument "{ident}"'
        return ident_to_arg[ident]

    idx = 0
    for el in macro.body:
        if isinstance(el, Op):
            asm.append(el)
        elif isinstance(el, LabelDef):
            asm.extend([Mark(labels[el.ident]), Op(OP_MAP['jumpdest'], b'')])
        elif isinstance(el, GeneralRef):
            asm.append(lookup_label(el.ident))
        elif isinstance(el, MacroParam):
            asm.append(lookup_arg(el.ident))
        elif isinstance(el, ConstRef):
            assert el.ident in g.constants, f'Constant "{el.ident}" not found'
            asm.append(g.constants[el.ident])
        elif isinstance(el, Invocation):
            if el.ident in BUILT_INS:
                asm.extend(
                    invoke_built_in(el.ident, g, el.args)
                )
            else:
                invoke_args: list[MacroArg] = []
                for arg in el.args:
                    if isinstance(arg, GeneralRef):
                        invoke_args.append(lookup_label(arg.ident))
                    elif isinstance(arg, MacroParam):
                        invoke_args.append(lookup_arg(arg.ident))
                    elif isinstance(arg, Op):
                        invoke_args.append(arg)
                    else:
                        raise TypeError(
                            f'Unrecognized macro invocation argument {arg}')

                asm.extend(
                    expand_macro_to_asm(
                        el.ident,
                        g,
                        invoke_args,
                        labels.copy(),
                        ctx_id + (idx, ),
                        visited_macros
                    )
                )
                idx += 1
        else:
            raise TypeError(f'Unrecognized macro element {el}')

    return asm

py_huff/codegen.py
Philogy/py-huff
from typing import NamedTuple, Iterable
from collections import defaultdict
from .opcodes import Op
from .node import ExNode
from .lexer import lex_huff
from .parser import (
    Identifier, CodeTable, Macro, get_ident, parse_hex_literal,
    bytes_to_push, parse_macro, get_includes
)
from .resolver import resolve
from .codegen import GlobalScope, expand_macro_to_asm, START_SUB_ID, END_SUB_ID
from .assembler import asm_to_bytecode, Mark, minimal_deploy

CompileResult = NamedTuple(
    'CompileResult',
    [
        ('runtime', bytes),
        ('deploy', bytes)
    ]
)


def idefs_to_defs(idefs: Iterable[ExNode]) -> dict[str, list[ExNode]]:
    defs: dict[str, list[ExNode]] = defaultdict(list)
    for d in idefs:
        defs[d.name].append(d)
    return defs


def compile(entry_fp: str) -> CompileResult:
    return compile_from_defs(idefs_to_defs(resolve(entry_fp)))


def compile_src(src: str) -> CompileResult:
    root = lex_huff(src)
    includes, idefs = get_includes(root)
    assert not includes, f'Cannot compile directly from source if it contains includes'
    return compile_from_defs(idefs_to_defs(idefs))


def compile_from_defs(defs: dict[str, list[ExNode]]) -> CompileResult:

    # TODO: Make sure constants, macros and code tables are unique
    constants: dict[Identifier, Op] = {
        get_ident(const): bytes_to_push(parse_hex_literal(const.get('hex_literal')))
        for const in defs['const']
    }
    macros: dict[Identifier, Macro] = {
        (macro := parse_macro(node)).ident: macro
        for node in defs['macro']
    }

    assert 'CONSTRUCTOR' not in macros, 'Custom constructors not yet supported'

    # TODO: Warn when literal has odd digits
    code_tables: dict[Identifier, CodeTable] = {
        get_ident(node): CodeTable(parse_hex_literal(node.get('hex_literal')), i)
        for i, node in enumerate(defs['code_table'], start=1)
    }

    functions: dict[Identifier, ExNode] = {
        get_ident(fn): fn
        for fn in defs['function']
    }

    events: dict[Identifier, ExNode] = {
        get_ident(e): e
        for e in defs['event']
    }

    assert 'MAIN' in macros, 'Program must contain MAIN macro entry point'

    asm = expand_macro_to_asm(
        'MAIN',
        GlobalScope(macros, constants, code_tables, functions, events),
        [],
        {},
        (0,),
        tuple()
    )

    for table in code_tables.values():
        asm.extend([
            Mark(((table.top_level_id,), START_SUB_ID)),
            table.data,
            Mark(((table.top_level_id,), END_SUB_ID)),
        ])

    runtime = asm_to_bytecode(asm)
    deploy = minimal_deploy(runtime)

    return CompileResult(
        runtime=runtime,
        deploy=deploy
    )

py_huff/compile.py
Philogy/py-huff
from parsimonious.grammar import Grammar
from parsimonious.nodes import Node
from .node import ExNode, Content


HUFF_GRAMMAR = Grammar(
    fr'''
    program = gap (definition gap)*
    definition = macro / include / const / code_table / function / event / error / jump_table

    comment = line_comment / multi_line_comment
    line_comment = "//" ~"[^\n]*"
    multi_line_comment = "/*" (("*" !"/") / ~"[^*]")* "*/"

    include = "#include \"" ~"([a-zA-Z0-9_-]|/|\.)+" "\""

    const = "#define" ws "constant" ws identifier ws "=" ws (hex_literal / "FREE_STORAGE_POINTER()")

    code_table = "#define" ws "table" ws identifier ws "{{" gap hex_literal gap "}}"

    function = "#define" ws "function" ws identifier ws tuple ws ("view" / "nonpayable" / "payable") ws "returns" ws tuple

    event = "#define" ws "event" ws identifier ws "(" ws (event_arg ws "," ws )* event_arg? ws ")"
    event_arg = type ws "indexed"? ws identifier?

    error = "#define" ws "error" ws identifier ws tuple

    jump_table = "#define" ws "jumptable" "__packed"? ws identifier ws "{{" gap (identifier gap)+ "}}"

    macro = "#define" ws macro_type ws identifier ws params ws "=" ws macro_returns_takes ws "{{" macro_body "}}"
    macro_type = "macro" / "fn"
    macro_returns_takes = "takes" ws "(" num ")" ws "returns" ws "(" num ")"
    macro_body = ws (macro_body_el ws) *
    macro_body_el = dest_definition / hex_literal / push_op / macro_arg / const_ref / invocation / identifier / comment
    push_op = "push" num ws hex_literal
    dest_definition = identifier ":"
    invocation = identifier ws "(" ws (call_arg ws "," ws)* call_arg? ws ")"
    call_arg = macro_arg / identifier / hex_literal / push_op
    macro_arg = "<" ws identifier ws ">"
    const_ref = "[" ws identifier ws "]"

    type = (("uint" num?) / ("bytes" num?) / "string" / "address" / tuple) ("[" ws num? ws "]") ?
    tuple = "(" ws (type ws identifier? ws "," ws )* (type ws identifier?)? ws ")"

    params = "(" ws param_list ws ")"
    param_list = ws (identifier ws "," ws)* identifier?

    identifier = ~"[a-zA-Z_][a-zA-Z0-9_]*"
    num = ~"[1-9][0-9]*" / "0"
    hex_literal = "0x" ~"[a-fA-F0-9]+"
    gap = ws (comment ws)*
    ws = ~"\s*"
    '''
)


def to_ex_node(node: Node, prune: frozenset[str] = frozenset()) -> ExNode:
    '''Converts parsimonious node to as simpler, nested tuple "ExNode"'''
    name = node.expr_name
    final_children: Content = node.text
    if node.children:
        children: list[ExNode] = []
        for child in node.children:
            if child.expr_name in prune:
                continue
            ex_child = to_ex_node(child, prune)
            if ex_child.content:
                children.append(ex_child)
        if len(children) == 1:
            if name == '':
                return children[0]

            if children[0].name == '':
                final_children = children[0].content
            else:
                final_children = children
        else:
            final_children = children

    return ExNode(name, final_children or '', node.start, node.end)


def lex_huff(s: str) -> ExNode:
    node = HUFF_GRAMMAR.parse(s)
    return to_ex_node(node, prune=frozenset({'ws', 'gap', 'comment'}))

py_huff/lexer.py
Philogy/py-huff
from typing import NamedTuple, Generator, Union
from enum import Enum

Content = list['ExNode'] | str


class ContentType(Enum):
    Text = 'Text'
    SubNodes = 'SubNodes'


class ExNode (NamedTuple(
    'ExNode',
    [
        ('name', str),
        ('content', Content),
        ('start', int),
        ('end', int)
    ]
)):
    def ctype(self) -> ContentType:
        if isinstance(self.content, list):
            return ContentType.SubNodes
        else:
            return ContentType.Text

    def children(self) -> list['ExNode']:
        if not isinstance(self.content, list):
            raise TypeError(f'Content of node is str not sub nodes')
        return self.content

    def text(self) -> str:
        if not isinstance(self.content, str):
            raise TypeError(f'Node has sub nodes, no direct string content')
        return self.content

    def get_all_deep(self, name) -> Generator['ExNode', None, None]:
        if isinstance(self.content, str):
            return
        assert isinstance(self.content, list)
        for child in self.content:
            if child.name == name:
                yield child
            else:
                yield from child.get_all_deep(name)

    def get_all(self, name: str) -> Generator['ExNode', None, None]:
        return (
            child
            for child in self.children()
            if child.name == name
        )

    def maybe_get(self, name: str) -> Union['ExNode', None]:
        matches = list(self.get_all(name))
        if len(matches) > 1:
            raise ValueError(
                f'{len(matches)} instances of "{name}" found, expectd 1'
            )
        if matches:
            return matches[0]
        else:
            return None

    def get(self, name: str) -> 'ExNode':
        gotten = self.maybe_get(name)
        if gotten is None:
            raise ValueError(f'"{name}" not found')
        return gotten

    def get_idx(self, i: int) -> 'ExNode':
        return self.children()[i]

    def _disp(self, rem_depth=-1, depth=0):
        if isinstance(self.content, list):
            print(f'{"  " * depth}[{self.name}]')
            if not rem_depth:
                return
            for child in self.content:
                child._disp(rem_depth-1, depth+1)
        else:
            print(f'{"  " * depth}[{self.name}] {self.content!r}')

py_huff/node.py
Philogy/py-huff
from typing import NamedTuple, Generator

OP_MAP = {
    'stop': 0x00,
    'add': 0x01,
    'mul': 0x02,
    'sub': 0x03,
    'div': 0x04,
    'sdiv': 0x05,
    'mod': 0x06,
    'smod': 0x07,
    'addmod': 0x08,
    'mulmod': 0x09,
    'exp': 0x0a,
    'signextend': 0x0b,
    'lt': 0x10,
    'gt': 0x11,
    'slt': 0x12,
    'sgt': 0x13,
    'eq': 0x14,
    'iszero': 0x15,
    'and': 0x16,
    'or': 0x17,
    'xor': 0x18,
    'not': 0x19,
    'byte': 0x1a,
    'shl': 0x1b,
    'shr': 0x1c,
    'sar': 0x1d,
    'sha3': 0x20,
    'address': 0x30,
    'balance': 0x31,
    'origin': 0x32,
    'caller': 0x33,
    'callvalue': 0x34,
    'calldataload': 0x35,
    'calldatasize': 0x36,
    'calldatacopy': 0x37,
    'codesize': 0x38,
    'codecopy': 0x39,
    'gasprice': 0x3a,
    'extcodesize': 0x3b,
    'extcodecopy': 0x3c,
    'returndatasize': 0x3d,
    'returndatacopy': 0x3e,
    'extcodehash': 0x3f,
    'blockhash': 0x40,
    'coinbase': 0x41,
    'timestamp': 0x42,
    'number': 0x43,
    'difficulty': 0x44,
    'prevrandao': 0x44,
    'gaslimit': 0x45,
    'chainid': 0x46,
    'selfbalance': 0x47,
    'basefee': 0x48,
    'pop': 0x50,
    'mload': 0x51,
    'mstore': 0x52,
    'mstore8': 0x53,
    'sload': 0x54,
    'sstore': 0x55,
    'jump': 0x56,
    'jumpi': 0x57,
    'pc': 0x58,
    'msize': 0x59,
    'gas': 0x5a,
    'jumpdest': 0x5b,
    'tload': 0x5c,
    'tstore': 0x5d,
    'push0': 0x5f,
    'push1': 0x60,
    'push2': 0x61,
    'push3': 0x62,
    'push4': 0x63,
    'push5': 0x64,
    'push6': 0x65,
    'push7': 0x66,
    'push8': 0x67,
    'push9': 0x68,
    'push10': 0x69,
    'push11': 0x6a,
    'push12': 0x6b,
    'push13': 0x6c,
    'push14': 0x6d,
    'push15': 0x6e,
    'push16': 0x6f,
    'push17': 0x70,
    'push18': 0x71,
    'push19': 0x72,
    'push20': 0x73,
    'push21': 0x74,
    'push22': 0x75,
    'push23': 0x76,
    'push24': 0x77,
    'push25': 0x78,
    'push26': 0x79,
    'push27': 0x7a,
    'push28': 0x7b,
    'push29': 0x7c,
    'push30': 0x7d,
    'push31': 0x7e,
    'push32': 0x7f,
    'dup1': 0x80,
    'dup2': 0x81,
    'dup3': 0x82,
    'dup4': 0x83,
    'dup5': 0x84,
    'dup6': 0x85,
    'dup7': 0x86,
    'dup8': 0x87,
    'dup9': 0x88,
    'dup10': 0x89,
    'dup11': 0x8a,
    'dup12': 0x8b,
    'dup13': 0x8c,
    'dup14': 0x8d,
    'dup15': 0x8e,
    'dup16': 0x8f,
    'swap1': 0x90,
    'swap2': 0x91,
    'swap3': 0x92,
    'swap4': 0x93,
    'swap5': 0x94,
    'swap6': 0x95,
    'swap7': 0x96,
    'swap8': 0x97,
    'swap9': 0x98,
    'swap10': 0x99,
    'swap11': 0x9a,
    'swap12': 0x9b,
    'swap13': 0x9c,
    'swap14': 0x9d,
    'swap15': 0x9e,
    'swap16': 0x9f,
    'log0': 0xa0,
    'log1': 0xa1,
    'log2': 0xa2,
    'log3': 0xa3,
    'log4': 0xa4,
    'create': 0xf0,
    'call': 0xf1,
    'callcode': 0xf2,
    'return': 0xf3,
    'delegatecall': 0xf4,
    'create2': 0xf5,
    'staticcall': 0xfa,
    'revert': 0xfd,
    'invalid': 0xfe,
    'selfdestruct': 0xff,
}


class Op(NamedTuple('Op', [('op', int), ('extra_data', bytes)])):
    def get_bytes(self) -> Generator[int, None, None]:
        yield self.op
        yield from self.extra_data


def create_plain_op(op_name: str) -> Op:
    assert not op_name.startswith('push') or op_name == 'push0', \
        f'Standalone {op_name} not supported'
    return Op(OP_MAP[op_name], b'')


def create_push(data: bytes, size: int | None = None) -> Op:
    if size is None:
        while len(data) > 1 and data[0] == 0:
            data = data[1:]
        size = len(data)
    else:
        assert len(data) <= size, \
            f'Expected data to be no more than {size} bytes long, got {len(data)}'
        padding = b'\x00'*(size - len(data))
        data = padding + data
    assert size in range(1, 32 + 1), f'No push of size {size}'
    return Op(0x5f + size, data)

py_huff/opcodes.py
Philogy/py-huff
from typing import NamedTuple, Generator
from .node import ExNode, Content
from .opcodes import Op, OP_MAP, create_plain_op, create_push

Identifier = str
MacroParam = NamedTuple('MacroParam', [('ident', Identifier)])
GeneralRef = NamedTuple('GeneralRef', [('ident', Identifier)])
ConstRef = NamedTuple('ConstRef', [('ident', Identifier)])
LabelDef = NamedTuple('LabelDef', [('ident', Identifier)])
InvokeArg = Op | GeneralRef | MacroParam
Invocation = NamedTuple(
    'Invocation',
    [('ident', Identifier), ('args', list[InvokeArg])]
)

MacroElement = Invocation | Op | MacroParam | GeneralRef | LabelDef | ConstRef

Macro = NamedTuple('Macro', [
    ('ident', Identifier),
    ('params', list[Identifier]),
    ('body', list[MacroElement])
])

CodeTable = NamedTuple(
    'CodeTable',
    [
        ('data', bytes),
        ('top_level_id', int)
    ]
)


def identifier(s: Content) -> Identifier:
    assert isinstance(s, str), f'Cannot create identifier from {s}'
    assert s not in OP_MAP, f'Valid opcode {s} cannot be identifier'
    return s


def function_to_sig(fn: ExNode) -> str:
    assert fn.name == 'function'
    args, _ = fn.get_all('tuple')
    return f'{get_ident(fn)}{tuple_to_compact_sig(args)}'


def event_to_sig(fn: ExNode) -> str:
    name = get_ident(fn)
    args = fn.get_all_deep('event_arg')
    parsed_args = [
        parse_type_to_sig(a.get("type"))
        for a in args
    ]
    return f'{name}({",".join(parsed_args)})'


def parse_type_to_sig(t: ExNode) -> str:
    if isinstance(t.content, str):
        return 'uint256' if t.content == 'uint' else t.content
    if t.name == 'tuple':
        return tuple_to_compact_sig(t)
    assert len(t.content) == 2, f'{t} not len 2'
    prim, snd = t.content
    if snd.name == 'num':
        base_type = prim.text()
        if base_type == 'uint':
            assert snd.text() in [*map(str, range(8, 256+1, 8))]
        elif base_type == 'bytes':
            assert snd.text() in [*map(str, range(1, 32+1))]
        else:
            raise ValueError(f'Unrecognized type with num {t.name}')
        return prim.text() + snd.text()
    else:
        children = snd.children()
        assert children[0].text() == '[' and children[-1].text() == ']' and len(children) in (2, 3), \
            'Dual node not bracket'
        if len(children) == 3:
            assert children[1].text() != '0', f'Array quantifier cannot be 0'
        return f'{parse_type_to_sig(prim)}{"".join(c.text() for c in children)}'


def tuple_to_compact_sig(node: ExNode) -> str:
    types = node.get_all_deep('type')
    return f'({",".join(map(parse_type_to_sig, types))})'


def get_ident(node: ExNode) -> Identifier:
    return identifier(node.get('identifier').text())


def literal_to_bytes(lit: str) -> bytes:
    return bytes.fromhex('0' * (len(lit) % 2) + lit)


def bytes_to_push(data: bytes) -> Op:
    if len(data) == 1 and data[0] == 0:
        return create_plain_op('push0')
    return create_push(data)


def parse_hex_literal(el: ExNode) -> bytes:
    assert el.name == 'hex_literal'
    lit = el.get_idx(1).text()
    return literal_to_bytes(lit)


def parse_call_arg(arg: ExNode):
    el = parse_el(arg)
    assert isinstance(el, (GeneralRef, Op, MacroParam)), \
        f'Invalid call argument {el}'
    return el


def parse_el(el: ExNode) -> MacroElement:
    el = el.get_idx(0)
    name = el.name
    if name == 'hex_literal':
        return bytes_to_push(parse_hex_literal(el))
    elif name == 'macro_arg':
        return MacroParam(get_ident(el))
    elif name == 'invocation':
        return Invocation(
            get_ident(el),
            list(map(parse_call_arg, el.get_all_deep('call_arg')))
        )
    elif name == 'identifier':
        ident = el.text()
        if ident in OP_MAP:
            return create_plain_op(ident)
        else:
            return GeneralRef(identifier(ident))
    elif name == 'dest_definition':
        return LabelDef(get_ident(el))
    elif name == 'const_ref':
        return ConstRef(get_ident(el))
    elif name == 'push_op':
        num = int(el.get('num').text())
        data = parse_hex_literal(el.get('hex_literal'))
        return create_push(data, num)

    raise ValueError(f'Unrecognized el name "{name}"')


def parse_macro(node: ExNode) -> Macro:
    assert node.name == 'macro', 'Not macro'

    macro_type = node.get('macro_type').text()
    assert macro_type == 'macro', f'Macro type {macro_type} not yet supported'
    ident = get_ident(node)

    if (param_list := node.get('params').maybe_get('param_list')) is not None:
        args = [
            identifier(ident_node.text())
            for ident_node in param_list.get_all_deep('identifier')
        ]
    else:
        args = []

    if (macro_body := node.maybe_get('macro_body')) is not None:
        els = [
            parse_el(raw_el)
            for raw_el in macro_body.get_all('macro_body_el')
        ]
    else:
        els = []

    # Validate macro argument references
    for el in els:
        if not isinstance(el, MacroParam):
            continue
        assert el.ident in args, f'Invalid macro arg {el.ident} for {ident} ({args})'

    return Macro(ident, args, els)


def get_defs(root: ExNode, name: None | str = None) -> Generator[ExNode, None, None]:
    for d in root.get_all('definition'):
        inner = d.get_idx(0)
        if name is None or inner.name == name:
            yield inner


def get_includes(root: ExNode) -> tuple[list[str], list[ExNode]]:
    includes: list[str] = []
    other_nodes: list[ExNode] = []
    for d in get_defs(root):
        if d.name == 'include':
            includes.append(d.get_idx(1).text())
        else:
            other_nodes.append(d)

    return includes, other_nodes

py_huff/parser.py
Philogy/py-huff
import os
from typing import Generator
from .lexer import lex_huff
from .node import ExNode
from .parser import get_includes


def resolve(fp: str, visited_paths: tuple[str, ...] = tuple()) -> Generator[ExNode, None, None]:
    fp = os.path.abspath(fp)
    assert fp not in visited_paths, f'Circular include in {fp}'
    visited_paths += (fp,)
    with open(fp, 'r') as f:
        file_root = lex_huff(f.read())

    includes, file_defs = get_includes(file_root)
    for include in includes:
        yield from resolve(os.path.join(os.path.dirname(fp), include))
    yield from file_defs

py_huff/resolver.py
Philogy/py-huff
from typing import TypeVar, Iterable, Callable, Any

T = TypeVar('T')
K = TypeVar('K')
V = TypeVar('V')


def default_unique_error(ident: Any) -> str:
    return f'Duplicate key {ident}'


DupErrorGen = Callable[[K], str]


def set_unique(d: dict[K, V], k: K, v: V,  on_dup: DupErrorGen = default_unique_error) -> dict[K, V]:
    assert k not in d, on_dup(k)
    d[k] = v
    return d


def build_unique_dict(
        kvs: Iterable[tuple[K, V]],
        on_dup: DupErrorGen = default_unique_error
) -> dict[K, V]:
    new_dict: dict[K, V] = {}
    for key, value in kvs:
        new_dict = set_unique(new_dict, key, value, on_dup=on_dup)
    return new_dict

py_huff/utils.py
Philogy/py-huff
from setuptools import find_packages, setup


with open('README.md', 'r', encoding='utf-8') as f:
    long_description = f.read()


setup(
    # metadata
    name='py-huff',
    version='0.0.1',
    description='A compiler for the Huff EVM assembly language written in Python',
    long_description_content_type='text/markdown',
    long_description=long_description,
    py_modules=['py_huff'],
    packages=find_packages(include='py_huff*'),
    classifiers=[
        'Programming Language :: Python :: 3',
        'Programming Language :: Python :: 3.10'
    ],
    author='Philogy',
    url='https://github.com/Philogy/py-huff',
    # actual data
    entry_points={
        'console_scripts': ['huffy = py_huff.cli:main']
    },
    install_requires=[
        'parsimonious >= 0.9.0',
        'types-parsimonious >= 0.10.0.9',
        'pycryptodome >= 3.19.0'
    ],
    package_data={'py-huff': ['py.typed']}
)

setup.py
Philogy/py-huff
import os
from py_huff.compile import compile


def compile_test(fp: str, expected_deploy: str, expected_runtime: str):
    # Resolve relative path
    path = os.path.join(os.path.dirname(os.path.abspath(__file__)), fp)
    result = compile(path)
    assert result.deploy.hex() == expected_deploy.strip(), 'Deploy code failed to match'
    assert result.runtime.hex() == expected_runtime.strip(), 'Runtime code failed to match'


def test_simple():
    compile_test(
        fp='../examples/single_main.huff',
        expected_deploy='600d8060095f395ff3600362017389015f5260205ff3',
        expected_runtime='                 600362017389015f5260205ff3'
    )


def test_single_macro():
    compile_test(
        fp='../examples/single_macro.huff',
        expected_deploy='600b8060095f395ff35f35602035015952595ff3',
        expected_runtime='5f35602035015952595ff3'
    )


def test_simple_labels():
    compile_test(
        fp='../examples/simple_labels.huff',
        expected_deploy='60238060095f395ff35f35600181600c5760205ff35b5f915b91810190916001900380600f57915f52595ff3',
        expected_runtime='5f35600181600c5760205ff35b5f915b91810190916001900380600f57915f52595ff3'

    )


def test_macro_args():
    compile_test(
        fp='../examples/macro_args.huff',
        expected_deploy='60278060095f395ff36040355f35602035908101818110602357905090810381811160235790505952595ff35b5f5ffd',
        expected_runtime='6040355f35602035908101818110602357905090810381811160235790505952595ff35b5f5ffd'
    )


def test_const_ref():
    compile_test(
        fp='../examples/const_ref.huff',
        expected_deploy='60048060095f395ff360826082',
        expected_runtime='60826082'
    )


def test_deep_args():
    compile_test(
        fp='../examples/deep_arg.huff',
        expected_deploy='60068060095f395ff36005565b5b5b',
        expected_runtime='6005565b5b5b'
    )


def test_simple_adjust():
    compile_test(
        fp='../examples/simple_adjust.huff', expected_deploy='61010480600a5f395ff36003565b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
        expected_runtime='6003565b00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'

    )


def test_includes():
    compile_test(
        fp='../examples/including.huff',
        expected_deploy='60078060095f395ff301010362123456',
        expected_runtime='01010362123456'
    )


def test_functions():
    compile_test(
        fp='../examples/functions.huff',
        expected_deploy='60188060095f395ff3630dbe671f63cd580ff363ea11c61d63c593ffcf628a99f3',
        expected_runtime='630dbe671f63cd580ff363ea11c61d63c593ffcf628a99f3'
    )


def test_events():
    compile_test(
        fp='../examples/events.huff', expected_deploy='60638060095f395ff37fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef7f884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a94243647f76fae7629f203ffe0facce0a6c4be1a3f7a1c29f37a5c3907738f3b5f9ab93bc',

        expected_runtime='7fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef7f884edad9ce6fa2440d8a54cc123490eb96d2768479d49ff9c7366125a94243647f76fae7629f203ffe0facce0a6c4be1a3f7a1c29f37a5c3907738f3b5f9ab93bc'
    )

test/test_main.py
Philogy/py-huff