├── LICENSE ├── README.md └── lambdascript └── __init__.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Thomas Baruchel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # lambdascript 2 | A new pure functional language built on the top of Python3. 3 | 4 | _Warning: this is an alpha release; the core of the interpreter is working and should give a precise idea of the language, but the provided program parses the `README.md` file (see the very last line of the code). This is because it should be discussed (on a mailing list) how to use the interpreter: as a standalone command line tool? as a module called from pure Python code? should it be turned into an interpreter or rather compile collection of functions to `.pyc` modules?_ In the initial days after the announcement of the project, I will be watching the `#lambdascript` channel on `irc.freenode.net` for discussing about the further evolutions of the project (answers may take a little time however). 5 | 6 | Lambdascript is a new languages (main influence being Haskell) intended to use several Python features and take some benefit of all modules written in Python while programming in a functional style. Since all expressions are Python expressions, one hour should be more than enough to understand the specific philosophy of the language. 7 | 8 | Once a public version will be released, it should be able to compile very well-written modules to be used in pure Python programs (or even in other Lambdascript programs). 9 | 10 | ## Philosophy 11 | 12 | Main features of the _Lambdascript_ programming language are: 13 | 14 | * strong emphasis on **literate programming** (a full program being a Markdown document structured with titles, well formatted paragraphs explaining each part of an algorithm, mathematical formulae, pictures, etc.); 15 | * **lexical binding** inside each block of code in the Markdown document in order to prevent bugs in case some names would be redefined; 16 | * **tail-recursion** support; 17 | * **currying** of all functions. 18 | 19 | It is intended to work either with CPython3 or PyPy3. 20 | 21 | ## Installing the module 22 | 23 | Just type `pip3 install lambdascript` for installing the module. Two functions are provided: `parse_block` (for evaluating a single block) and `parse_document` (for evaluating a whole markdown document). 24 | 25 | ## Using the interpreter 26 | 27 | The best starting point should be to run Python3 (or Pypy3) and type: `import lambdascript` and then `lambdascript.parse_document("README.md")` where the file `README.md` is the current file (which is a valid Lambdascript file). Then read carefully the current document in order to understand what is happening and make some changes in the `README.md` file in order to experiment (or change the last line of the program in order to adapt it to your needs). 28 | 29 | Of course, this will be improved now, but the initial goal was rather to make the language work. 30 | 31 | ## Language specifications 32 | 33 | ### Lexical binding 34 | 35 | A Lambdascript program is logically split into blocks of code in the whole Markdown document containing it. These blocks should be well commented by using all features of Markdown syntax, but it will be seen later that splitting the code into blocks is not only performed for esthetical reasons; it should also follow some logical principles since different decisions in splitting the code are not technically equivalent. 36 | 37 | A Lambdascript block of code is a comma-separated collection of declarations. Unlike Python, Lambdascript does not care at all about indentations; furthermore, it does not care either about order of declarations inside a given code block. An example of a simple block of code is: 38 | 39 | f: lambda n: 2*n, 40 | x: 42, 41 | g: lambda n: f(n)+x 42 | 43 | Since order does not matter, the very same block could be written: 44 | 45 | g: lambda n: f(n)+x 46 | # where 47 | , f: lambda n: 2*n 48 | , x: 42 49 | 50 | Each declaration being made of a label and any Python expression (generally a lambda function). 51 | 52 | In the example above, the most important thing is lexical binding: the three objects `f`, `x` and `g` will be mirrored to the global namespace but the binding by itself is performed in a separate protected namespace, meaning that later changing the content of the global variables `x` or `f` will never break the behaviour of the `g` function. This also apply for recursive functions: 53 | 54 | # Factorial function 55 | fac : lambda n: n*fac(n-1) if n else 1 56 | 57 | Of course, dynamic binding is still performed whenever a symbol was (previously) declared outside of the current block of code (either from pure Python code or as Lambdascript code). 58 | 59 | ### Symbols 60 | 61 | Any valid Python name is a Lambdascript valid name but: 62 | 63 | * a symbol beginning with exactly one underscore is lexically bound without being mirrored to the global namespace; 64 | * a symbol beginning with two underscores has special meaning and can not be used as an arbitrary name when programming; 65 | * unlike well-written pure Python code, it should be considered better here to use short single-letter names for auxiliary functions (like `f`, `g` or even `φ`) with a very clear explanation of their role in a Markdown paragraph and keep long explicit name for the main function in each block of code (two reasons for this: it is safe to re-use the same short names in different locations since they will be lexically bound and another idea is to follow mathematical usages and better integrate with mathematical equations, if any, in the Markdown document; furthermore, an explicit long name for the main function will allow the reader to locate it more easily). 66 | 67 | Since short names are encouraged, Unicode symbols may be used (for instance greek letters), but there was a bug in the current versions of PyPy3, concerning a rather obscure feature used by the Lambdascript interpreter and Unicode symbols are not supported if the interpreter is run with PyPy3 instead of CPython3 (this bug was [reported and fixed](https://bitbucket.org/pypy/pypy/issues/2457) however and next versions of PyPy3 should work fine in this case too). 68 | 69 | ### Functions and constants 70 | 71 | Since the binding between objects declared in the same block will never be broken, Lambdascript objects are all "constants" in some way; but for convenience reason, this word will be used only for non-lambda objects. Thus Lambdascript objects are either functions or constants. In the first example, `x` is a constant, which is very useful for writing the following block: 72 | 73 | area: lambda r: pi * r**2 74 | # where 75 | , pi: 3.14159 76 | 77 | Technically a constant is anything that is not _initially_ declared as a lambda object, even: 78 | 79 | f: (lambda: lambda n: 2*n)() 80 | 81 | An important rule is that constants can't be involved in circular dependency relations, while functions can; thus the following block is perfectly valid: 82 | 83 | f: lambda x: g(x-1)**2 if x else 1, 84 | g: lambda x: 2*f(x-1) if x else 1 85 | 86 | Furthermore, currying or tail-recursion optimization (see below) will not be applied on constants. 87 | 88 | ### Currying 89 | 90 | Currying is applied on all Lambdascript functions: 91 | 92 | add: lambda a,b: a+b, 93 | inc: add(1), 94 | __print__: inc(41) # will print 42 95 | 96 | ### Tail-recursion 97 | 98 | When a lambdascript function is tail-recursive, the recursive call is internally turned into a loop (the function is found to be tail-recursive if and only if all self-reference are tail-calls; optimization does not occur if tail-calls are mixed with non-tail-calls or even with non-called references). Thus loops are avoided in Lambdascript: 99 | 100 | fac: lambda n, f: fac(n-1, n*f) if n else f 101 | 102 | Of course the previous function could also be written by using an auxiliary function: 103 | 104 | _fac: lambda n, f: _fac(n-1, n*f) if n else f, 105 | fac: lambda n: _fac(n, 1) 106 | 107 | As an extra benefit, the choice of naming `_fac` the function above will prevent this function to be mirrored in the global namespace. (This is the rule for any symbol beginning with exactly _one_ underscore). 108 | 109 | ### Special symbols 110 | 111 | Special symbols will be extended; right now, only one symbol is provided: 112 | 113 | * `__print__` will print the associated expression _after_ the whole block has been parsed and evaluated; 114 | 115 | _TODO_ 116 | 117 | ### Mixing pure Python and Lambdascript 118 | 119 | Pure Python can be embed in the Markdown document by using _fenced code blocks_ and indicating `python` as the language of the block. A fenced code block can also have `lambdascript` as its language if required. 120 | 121 | The following piece of code is a fenced code block containing Python code (and using the previously defined `fac` function): 122 | 123 | ~~~python 124 | print(fac(5)) 125 | ~~~ 126 | -------------------------------------------------------------------------------- /lambdascript/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | A new pure functional language built on the top of Python3. 3 | """ 4 | 5 | __version__ = '0.1.3 alpha' 6 | # -*- coding: utf-8 -*- 7 | 8 | import ast, re 9 | 10 | class DuplicateDeclarationError(Exception): 11 | pass 12 | class CircularReferenceError(Exception): 13 | pass 14 | 15 | def __ast_check_tail_recursive__(node, symbol): 16 | # count all references of 'symbol' (even if not called) 17 | # count tail-calls of symbols 18 | n = sum((isinstance(w, ast.Name) and w.id==symbol) 19 | for w in ast.walk(node)) 20 | def count(no): 21 | if isinstance(no, ast.IfExp): 22 | return count(no.body) + count(no.orelse) 23 | if ( 24 | isinstance(no, ast.Call) 25 | and isinstance(no.func, ast.Name) 26 | and no.func.id == symbol ): 27 | return 1 28 | return 0 29 | return (n>0) and (n==count(node.body)) 30 | 31 | 32 | preamble = ast.parse(""" 33 | class __TailRecursiveCall__: 34 | def __init__(self, args): 35 | self.run = True 36 | self.args = args 37 | def __call__(self, *args): 38 | self.run = True 39 | self.args = args 40 | 41 | def __make_tail_recursive__(__func): 42 | def __run_function__(*args): 43 | __T__ = __TailRecursiveCall__(args) 44 | while __T__.run: 45 | __T__.run = False 46 | __result__ = __func(__T__)(*__T__.args) 47 | return __result__ 48 | return __run_function__ 49 | 50 | __make_curry__ = lambda f: (lambda n: 51 | (lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args)))) 52 | (lambda g: lambda *args: f(*args) if len(args) >= n 53 | else lambda *args2: g(*(args+args2))) 54 | )(f.__code__.co_argcount) 55 | """, mode='exec').body 56 | 57 | def parse_block(s, context=globals()): 58 | """ 59 | s : a (possibly multiline) string containing lambdascript code 60 | context : the context in which the functions are to be mirrored 61 | internal : lambdascript global variables 62 | """ 63 | # A lambdascript cell is like a Python dictionary without enclosing braces 64 | node = ast.parse('{'+s+'}', mode='eval').body 65 | # Extraction of names (some of them are reserved symbols 66 | names, reserved = {}, {} 67 | nonlambda = [] 68 | for k, v in zip([k.id for k in node.keys], node.values): 69 | if len(k) >= 2 and k[:2] == "__": 70 | if k in reserved: 71 | raise DuplicateDeclarationError( 72 | # TODO: find a better sentence 73 | "Several uses of the special symbol '%s'" 74 | + " in the same environment" 75 | % k ) 76 | reserved[k] = v 77 | else: 78 | if k in names: 79 | raise DuplicateDeclarationError( 80 | "Several declarations for the symbol '%s'" 81 | + " in the same environment" 82 | % k ) 83 | names[k] = v 84 | if not isinstance(v, ast.Lambda): 85 | nonlambda.append(k) 86 | else: # TODO 87 | pass 88 | # parse content of Lambda in order to find the tail-recursion 89 | # symbol ...( *args ) with Ellipsis(). See: 90 | # ast.dump(ast.parse("...(3)", mode='eval')) 91 | # 'Expression(body=Call(func=Ellipsis(), args=[Num(n=3)], keywords=[], starargs=None, kwargs=None))' 92 | # On pourra aussi chercher ...[k](3) pour la continuation 93 | # Extraction of free variables (but not global ones) 94 | freevars = {} 95 | body = [ 96 | ast.Assign(targets=[ast.Name(id=k, ctx=ast.Store())], 97 | value=ast.Lambda(args=ast.arguments( 98 | args=[], vararg=None, kwonlyargs=[], 99 | kw_defaults=[], kwarg=None, defaults=[]), 100 | body=ast.Num(n=0))) 101 | for k in names ] 102 | c = {} # local context 103 | for k in names: 104 | # We append a 'Lambda' in front of the expression in case it isn't a Lambda 105 | # itself (in order to avoid getting the expression evaluated) 106 | body.append(ast.Return( 107 | value=ast.Lambda(args=ast.arguments( 108 | args=[], varargs=None, kwonlyargs=[], 109 | kw_defaults=[], kwarg=None, defaults=[]), body=names[k]))) 110 | M = ast.Module(body=[ast.FunctionDef(name='__lambdascript__', 111 | args=ast.arguments(args=[], vararg=None, kwonlyargs=[], 112 | kw_defaults=[], kwarg=None, defaults=[]), body=body, 113 | decorator_list=[], returns=None)]) 114 | M = ast.fix_missing_locations(M) 115 | exec(compile(M, '', mode='exec'), context, c) 116 | body.pop() 117 | freevars[k] = c['__lambdascript__']().__code__.co_freevars 118 | # An O(n^2) algorithm for checking that non-lambda expressions are not 119 | # involved in circular dependancies (lambda expressions are allowed to be) 120 | for k in names: 121 | if k in nonlambda: 122 | checked = { k:False for k in names } 123 | stack = [k] 124 | while stack: 125 | i = stack.pop() 126 | checked[i] = True 127 | j = freevars[i] 128 | for e in j: 129 | if e==k: 130 | raise CircularReferenceError( 131 | "Symbol '"+k+"' involved in a circular reference relation" 132 | ) 133 | if not checked[e]: stack.append(e) 134 | # Tail-recursion 135 | for k in names: 136 | if k not in nonlambda and __ast_check_tail_recursive__(names[k], k): 137 | for w in ast.walk(names[k]): 138 | if isinstance(w, ast.Name) and w.id==k: 139 | w.id = k 140 | names[k] = ast.Lambda(args = ast.arguments( 141 | args=[ast.arg(arg=k, annotation=None)], vararg=None, 142 | kwonlyargs=[], kw_defaults=[], kwarg=None, defaults=[]), 143 | body= names[k]) 144 | names[k] = ast.Call(func=ast.Name(id='__make_tail_recursive__', 145 | ctx = ast.Load()), args=[names[k]], 146 | keywords=[], starargs=None, kwargs=None) 147 | # Curry 148 | for k in names: 149 | if k not in nonlambda: 150 | names[k] = ast.Call(func=ast.Name(id='__make_curry__', 151 | ctx = ast.Load()), args=[names[k]], 152 | keywords=[], starargs=None, kwargs=None) 153 | # Reference of a lambda in another lambda can now be safely removed 154 | # from the dictionary 'freevars' because sorting the declarations not 155 | # care about order between two lambda expressions. 156 | for k in names: 157 | if k not in nonlambda: 158 | freevars[k] = tuple( i for i in freevars[k] if i in nonlambda ) 159 | # Sort the declarations 160 | D = [] 161 | tmp = list(names) 162 | while tmp: 163 | for i in range(len(tmp)): 164 | e = tmp[i] 165 | ev = freevars[e] 166 | if all(i in D for i in ev): 167 | D.append(tmp.pop(i)) 168 | break 169 | # for/else: raise # useless after previous check 170 | # Compile all expressions 171 | body_outer = list(preamble) 172 | body_inner = [] 173 | for k in nonlambda: 174 | body_inner.append(ast.Nonlocal(names=[k])) 175 | for k in D: 176 | if k in nonlambda: 177 | body_outer.append(ast.Assign(targets=[ast.Name(id=k, 178 | ctx=ast.Store())], 179 | value=ast.Num(n=0))) 180 | body_inner.append(ast.Assign(targets=[ast.Name(id=k, 181 | ctx=ast.Store())], 182 | value=names[k])) 183 | else: 184 | body_outer.append(ast.Assign( 185 | targets=[ast.Name(id=k, ctx=ast.Store())], value=names[k])) 186 | body_inner.append(ast.Assign( 187 | targets=[ast.Attribute(value=ast.Name(id=k, ctx=ast.Load()), 188 | attr='__code__', ctx=ast.Store())], 189 | value=ast.Attribute(value=names[k], 190 | attr='__code__', ctx=ast.Load()))) 191 | body_inner.append(ast.Return(value=ast.Dict( 192 | keys=[ast.Str(s=k) for k in D], 193 | values=[ast.Name(id=k, ctx=ast.Load()) for k in D]))) 194 | body_outer.append(ast.FunctionDef(name='__inner__', args=ast.arguments( 195 | args=[], vararg=None, kwonlyargs=[], 196 | kw_defaults=[], kwarg=None, defaults=[]), 197 | body=body_inner, decorator_list=[], returns=None)) 198 | body_outer.append(ast.Return(value=ast.Call( 199 | func=ast.Name(id='__inner__', ctx=ast.Load()), 200 | args=[], keywords=[], starargs=None, kwargs=None))) 201 | M = ast.Module(body=[ast.FunctionDef(name='__lambdascript__', 202 | args=ast.arguments(args=[], vararg=None, kwonlyargs=[], 203 | kw_defaults=[], kwarg=None, defaults=[]), body=body_outer, 204 | decorator_list=[], returns=None)]) 205 | M = ast.fix_missing_locations(M) 206 | exec(compile(M, '', mode='exec'), context, c) 207 | S = c['__lambdascript__']() 208 | # mirror all symbols in context (generally globals()) 209 | # don't mirror private symbols 210 | for k in D: 211 | if k[0] != '_': context[k] = S[k] 212 | # Parse special symbols (AFTER) 213 | for k in reserved: 214 | if k == "__print__": 215 | E = ast.Expression(body=reserved[k]) 216 | print(eval(compile(E, '', mode='eval'), context, c)) 217 | 218 | 219 | def __markdown_parser(fname): 220 | in_block = False 221 | in_fenced = False 222 | last_empty = True 223 | re_empty_line = re.compile("^\s*$") 224 | re_code_line = re.compile("^( )|\t") 225 | re_fenced = re.compile("(?P[~`]{3,})\s*(?P[^\s`]+)?") 226 | block = "" 227 | fenced = "" 228 | lang = "" 229 | ls = 0 230 | with open(fname, mode='r') as f: 231 | for n, l in enumerate(f, start=1): 232 | todo = True 233 | while todo: 234 | todo = False 235 | if in_fenced: 236 | if len(l) >= len(fenced) and fenced == l[:len(fenced)]: 237 | in_fenced = False 238 | in_block = False 239 | last_empty = True 240 | yield (block, lang, ls, n) 241 | lang = "" 242 | else: block += l 243 | elif in_block: 244 | if re_code_line.match(l) or re_empty_line.match(l): 245 | block += l 246 | else: 247 | in_block = False 248 | yield (block, lang, ls, n-1) 249 | lang = "" 250 | todo = True 251 | elif last_empty and re_code_line.match(l): 252 | in_block = True 253 | last_empty = False 254 | lang = "lambdascript" 255 | block = l 256 | ls = n 257 | elif len(l) >= 3 and ( 258 | l[:3] == "~~~" or l[:3] == "```" ): 259 | last_empty = False 260 | fenced, lang = re_fenced.match(l).groups() 261 | if lang == None: lang = "lambdascript" 262 | in_block = True 263 | in_fenced = True 264 | block = "" 265 | ls = n 266 | elif re_empty_line.match(l): 267 | last_empty = True 268 | else: 269 | last_empty = False 270 | if in_block: yield (block, lang, ls, n) 271 | 272 | 273 | def parse_document(fname, context=globals()): 274 | for s, lang, ls, le in __markdown_parser(fname): 275 | try: 276 | if lang == "python": 277 | exec(s, context) 278 | elif lang == "lambdascript": 279 | parse_block(s, context=context) 280 | except Exception as e: 281 | print("Exception encountered during execution" 282 | + " of block at lines %d-%d:" % (ls, le)) 283 | raise e 284 | 285 | __all__ = ['parse_block', 'parse_document'] 286 | --------------------------------------------------------------------------------