Skip to content

TARDIInsanity/TDI_Honeycomb

Repository files navigation

TDI_Honeycomb

pythonic (COMBinator) lambda calculus esolang in python

This will not work outside of an IDE. There is no main, the best I can offer is the function "ipr" which takes a path and a dictionary and interprets the code in the file at that path.

In the file "suki_02_lexer.py":

This file depends on (https://github.com/TARDIInsanity/TDI_parse_integer : guaranteed to work with v2.3) to lex tokens
The class "Lexer" tests the classes, in order, in class attribute TOKENS. Each class is expected to implement .pull(cls, code:str) -> (success:bool, result:object, new_code:str). The first class to return a truthy success value is accepted and returned immediately, but EVERY class's new_code replaces the old code string, as it is assumed classes return the input code unmodified on a failure every time. Create a subclass of Lexer to get started lexing your own program. Default tokens made available, in descending order of priority: Comment (#...\n or ###...###), Indent (\n along with all following non-\n whitespace), Special ('=' and '->'), Paren (the three parentheses recognized by python: ()[]{}.), Punct (.,:;), Keyword (most flow control keywords in python, plus def, comb, dowhile, and do), Name (any identifier that does not match a Keyword), Integer (using TDI_parse_integer.IntPuller with the sole single_sep of "_"), String (any pythonic string, with the exception that concatenated strings are recognized as distinct (whereas in python "abc"'def' is recognized as the single string "abcdef", here the lexer would provide two strings))
The class "Lexer" implements .pop() -> Token and .push(val:Token) so that tokens may be lexed both lazily (.pop() causes the lexer to lex one more token, unless the push buffer has tokens, at which time it pops those tokens back) and without consequence (by combined use of .pop and .push, you can peek at arbitrarily many tokens before deciding whether you wish to keep them). Tokens themselves implement .be and .like to compare themselves to other tokens. See the implementation of the base class (Token) for the best possible understanding. It also now implements enter, accept, reject, and conclude methods for (programmer) efficient (though perhaps memory inefficient) backtracking. All tokens are stored in a buffer until .accept() or .conclude(True), at which time they are moved to the superceding buffer. .enter() prepares a new buffer. .reject(x)->x should be used before ALL raised errors, with the recommended style being "raise lexer.reject(...)". .conclude(False) and .reject() both push the tokens in the buffer back into the stream in the same order they were removed without error. When returning from a parse, the recommended style is (lexer.conclude(...), result). Before popping any tokens in a parse, it is heavily recommended to perform lexer.enter(), or else completely ignore these backtracking features, as to avoid causing trouble for other parse functions.

In the file "suki_02_parser_03.py":

This file depends on (suki_02_lexer) and creates its own lexer subclass, as well as using many token classes for comparisons and checks throughout the parser. This subclass implements .peek(comparison:Token) -> bool in order to efficiently peek at the very next token, a common task in parsing. The class "Parser" calls upon the class "Block" (representing an indented code block) to parse the whole file. The class "Block" repeatedly calls upon "Parser" (passed in as an argument, which means a different parser can actually be used in different contexts) to parse a singular statement, via the method (.pop_stmt). To parse a singular statement, (Parser) tests each class in each list in the class attribute STMTS. Each class is expected to implement .pull(cls, parser, lexer:Lexer) -> (success:bool, result:object), where it is expected to raise a SyntaxError if it cannot restore (lexer) to its original condition before returning with a failure, or if the parsed language's syntax demands an error in a given situation (such as not receiving a condition expression after the "if" keyword, or an unexpected "else" keyword appearing with no governing body). Whenever a statement requires an expression argument, it calls upon Exprs to parse an arbitrary nonzero number of expressions. Whenever an expression requires an expression argument, it should call upon (Parser.pop_expr) to do so. To parse code directly into an abstract syntax tree, call (Parser.parse(code) -> (success:bool, code_block:Block)). (code_block) implements .process(self, context:dict) -> Flag. If the code has any code-defined function calling whatsoever, (context) is expected to implement .newsub(self) to produce a (subcontext) which falls back on (context) whenever a key cannot be found in (subcontext).

In the file "suki_02.py":

This file depends on (suki_02_parser_03) and creates its own Context class satisfying the requirements stated above. It also creates a subclass of (Parser) which prefixes the code with a newline to guarantee that the initial statement pop (which looks for an Indent) can find a newline as the first char of the code. This file defines some handy functions such as (nj) which joins strings with newline, (unfold_human) which takes a tuple of integers and tuples of integers and ... and unfolds it into a linearized tuple of integers, with -1 representing open paren and -2 representing close paren. This representation is exactly what is required to initialize a new instance of ConstComb, an Expression(AST) subclass meant to behave like a lambda calculus proper combinator, and which gives this programming language its name: honeyCOMB. This file initializes many common combinators, such as the turing-complete sets S K I, B C K W, as well as common helpful combinators such as V, O, WC (named Yi), and Yb (comb y f: f (y y f)), with the property that Yb Yb behaves identically to the Y combinator. The current implementation will not evaluate the arguments of functions beyond their initial variable reference grabbing until the function has all the arguments it requires. Use this very carefully to implement recursive lambda functions if you so wish by passing in a function of at least two arguments which, somewhere in its expression, evaluates the first argument with some value(s) which is/are not identical to the second argument(s). Then, this new object, whenever it is given N-1 arguments (for N being how many arguments the passed function takes), it will evaluate the function recursively. This can be frustrating at times, given that functions which are meant to return recursive structures simply won't evalaute beyond the very first item until you start pulling items off, but it is a necessary implementation detail for lambda calculus. Non-lambda functions may also be defined, but they behave according to an entirely different paradigm as a design choice. These functions implement recursion in a much more traditional way by interacting directly with the variable namespace. Functions may be passed as arguments and returned from other functions, but may not otherwise be modified or inspected.

About

pythonic (COMBinator) lambda calculus esolang in python

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages