Add files via upload
This commit is contained in:
745
ipynb/lispy.ipynb
Normal file
745
ipynb/lispy.ipynb
Normal file
@@ -0,0 +1,745 @@
|
|||||||
|
{
|
||||||
|
"cells": [
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "61b75f91-0729-4bc0-a107-f77a4a62eb98",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"<div align=\"right\" style=\"text-align: right\"><i>Peter Norvig</i></div>\n",
|
||||||
|
"\n",
|
||||||
|
"# (How to Write a (Lisp) Interpreter (in Python))\n",
|
||||||
|
"\n",
|
||||||
|
"This notebook describes how to implement computer language interpreters in general, and in particular an interpreter for most of the [**Scheme**](https://www.scheme.org/) dialect of [**Lisp**](https://en.wikipedia.org/wiki/Lisp_(programming_language%29). I call my language and interpreter **Lispy** because it is Lisp implemented in Python (lis.py). Years ago, I showed how to write a semi-practical near-complete Scheme interpreter (one in [Java](https://norvig.com/jscheme.html) and one in [Common Lisp](https://github.com/norvig/paip-lisp/blob/main/docs/chapter22.md)). This time the goal is simplicity. \n",
|
||||||
|
"\n",
|
||||||
|
"Why should interpreters and compilers matter to you? As [Steve Yegge said](https://steve-yegge.blogspot.com/2007/06/rich-programmer-food.html?), \"If you don't know how compilers work, then you don't know how computers work.\" Yegge describes 8 problems that can be solved with compilers (or equally well with interpreters, or with Yegge's typical heavy dosage of cynicism).\n",
|
||||||
|
"\n",
|
||||||
|
"## Syntax and Semantics of Programs\n",
|
||||||
|
"\n",
|
||||||
|
"The syntax of a language is the arrangement of characters to form correct statements or expressions. For example, in the language of mathematical expressions (and in many programming languages), the syntax for adding one plus two is \"1 + 2\". The semantics of a language determines what it means. We say we are evaluating an expression when we determine its value; we would say that \"1 + 2\" evaluates to 3, and write that as \"1 + 2\" ⇒ 3. \n",
|
||||||
|
"\n",
|
||||||
|
"Scheme syntax is different from most other programming languages. Consider:\n",
|
||||||
|
"\n",
|
||||||
|
" Java\t \t | Scheme\n",
|
||||||
|
" ----------------------------------------------+-------------------------------\n",
|
||||||
|
" if (x.val() > 0) { | (if (> (val x) 0)\n",
|
||||||
|
" return fn(A[i] + 3 * i, | (fn (+ (aref A i) (* 3 i))\n",
|
||||||
|
" new String[] {\"one\", \"two\"}); | (quote (one two))))\n",
|
||||||
|
" } |\n",
|
||||||
|
"\n",
|
||||||
|
"Java has a wide variety of syntactic conventions (keywords, infix operators, three kinds of brackets, operator precedence, dot notation, quotes, commas, semicolons), but Scheme syntax is much simpler:\n",
|
||||||
|
"Scheme programs consist solely of expressions; there is no statement/expression distinction.\n",
|
||||||
|
"Numbers (e.g. 1) and symbols (e.g. A) are called atomic expressions; they cannot be broken into pieces. These are similar to their Java counterparts, except that in Scheme, operators such as `+` and `>` are symbols too, and are treated the same way as `A` and `fn`.\n",
|
||||||
|
"Everything else is a list expression: a \"(\", followed by zero or more expressions, followed by a \")\". The first element of the list determines what it means:\n",
|
||||||
|
"- A list starting with a keyword, e.g. `(if ...)`, is a **special form**; the meaning depends on the keyword.\n",
|
||||||
|
"- A list starting with a non-keyword, e.g. `(sqrt x)`, is a function call: the function `sqrt` is applied to the argument, `x`, to compute a value.\n",
|
||||||
|
"\n",
|
||||||
|
"The beauty of Scheme is that the full language only needs 5 keywords and 8 syntactic forms. In comparison, Python has 33 keywords and 110 syntactic forms, and Java has 50 keywords and 133 syntactic forms. All those parentheses may seem intimidating, but Scheme syntax has the virtues of simplicity and consistency. (Some have joked that \"Lisp\" stands for \"**L**ots of **I**rritating **S**illy **P**arentheses\"; I think it stand for \"**L**isp **I**s **S**yntactically **P**ure\".)\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"How do we go from syntax to semantics? Here is the traditional approach:\n",
|
||||||
|
"- **Parsing**: A function called `parse` takes the program (a sequence of characters) as input and transforms it into an internal representation, called an **abstract syntax tree**, that closely mirrors the nested structure of statements or expressions in the program. This will be done in two steps. First, the function`tokenize` breaks up the characters into tokens (symbols such as parentheses, numbers such as `123`, and symbols such as `if`).Second, the function `read_from_tokens` converts the tokens into the abstract syntax tree.\n",
|
||||||
|
"- **Execution**: A function called `eval` evaluates the syntax tree to produce the value of the program. \n",
|
||||||
|
"\n",
|
||||||
|
"# Language 1: Lispy Calculator\n",
|
||||||
|
"\n",
|
||||||
|
"We won't tackle all of Scheme right away; instead we'll start with a subset of Scheme I call **Lispy Calculator**. Lispy Calculator lets you do any computation you could do on a typical calculator—as long as you are comfortable with prefix notation. And you can do two things that are not offered in most calculators: use an `if` to define a conditional expression, and use a `define` to set the value of a new variable. Here's an example program, that computes the area of a circle of radius 10, using the formula π r<sup>2</sup>\n",
|
||||||
|
"\n",
|
||||||
|
" (begin\n",
|
||||||
|
" (define r 10)\n",
|
||||||
|
" (* pi (* r r)))\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"Here is a table of all the allowable expressions in the Lispy Calculator Language. In the Syntax column of this table, *symbol* must be a symbol, *number* must be an integer or floating point number, and the other italicized words can be any expression. The notation *arg...* means zero or more repetitions of *arg*.\n",
|
||||||
|
"\n",
|
||||||
|
"|Expression\t|Syntax\t|Example|Semantics|\n",
|
||||||
|
"|-----------|-------|-------|-------------|\n",
|
||||||
|
"|constant literal\t|*number*\t|`12` or `-3.45e+6`|A number evaluates to itself.|\n",
|
||||||
|
"|variable reference|\t*symbol*\t|`r`|A symbol is interpreted as a variable name; its value is the variable's value.|\n",
|
||||||
|
"|conditional\t|(`if` *test then_part else_part*`)`|`(if (< x 0) (- x) x)`|\tEvaluate test; if true, evaluate and return conseq; otherwise alt.|\n",
|
||||||
|
"|definition\t|`(define` *symbol exp*`}`\t|`(define r 10)`|Define a new variable and give it the value of evaluating the expression exp.|\n",
|
||||||
|
"|procedure call\t|`(`*proc arg*...`)`\t|`(sqrt (* 2 8))` ⇒ 4.0|Evaluate proc and all the args, and then the procedure is applied to the list of arg values.|\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"Let's get some imports out of the way, and be explicit about our representations for Scheme object types and their representation in Python:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 1,
|
||||||
|
"id": "e1c3cef0-d091-433e-a2c0-0959df3cee0d",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"import numbers\n",
|
||||||
|
"import math\n",
|
||||||
|
"import operator as op\n",
|
||||||
|
"\n",
|
||||||
|
"Number = numbers.Number # A Scheme Number is implemented as a Python number (e.g., int or float)\n",
|
||||||
|
"Symbol = str # A Scheme Symbol is implemented as a Python str\n",
|
||||||
|
"List = list # A Scheme List is implemented as a Python list\n",
|
||||||
|
"Atom = Symbol | Number # A Scheme Atom is a Symbol or Number\n",
|
||||||
|
"Exp = Atom | List # A Scheme expression is an Atom or List\n",
|
||||||
|
"Env = dict[Symbol, Exp] # A Scheme environment is a dictionary mapping of {variable: value}"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "b5c1ff1c-15e1-47d2-bfc8-a008ecc5ff13",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Evaluation: eval \n",
|
||||||
|
"\n",
|
||||||
|
"Here is the core of the interpreter, `eval`. It takes as input an expression, `exp`, and an **environment** that specifies the values of variables. It returns the value of the expression, given the environment. These few lines are what what [Alan Kay called](https://queue.acm.org/detail.cfm?id=1039523) \"the Maxwell's Equations of Software.\"\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 2,
|
||||||
|
"id": "3b081873-6ae7-4d73-830d-c441cd196cbc",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def eval(exp: Exp, env: Env) -> object:\n",
|
||||||
|
" \"\"\"Evaluate an expression in an environment.\"\"\"\n",
|
||||||
|
" match exp:\n",
|
||||||
|
" case Number(): # number evaluates to itself \n",
|
||||||
|
" return exp\n",
|
||||||
|
" case Symbol(): # variable evaluates to its value in environment\n",
|
||||||
|
" return env[exp]\n",
|
||||||
|
" case ('if', test, then, els): # conditional evaluates one branch or the other\n",
|
||||||
|
" branch = (then if eval(test, env) \n",
|
||||||
|
" else els)\n",
|
||||||
|
" return eval(branch, env)\n",
|
||||||
|
" case ('define', name, exp): # definition adds name to the environment\n",
|
||||||
|
" env[name] = eval(exp, env)\n",
|
||||||
|
" case (proc, *args): # procedure call\n",
|
||||||
|
" func = eval(proc, env)\n",
|
||||||
|
" vals = [eval(arg, env) for arg in args]\n",
|
||||||
|
" return func(*vals)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "edafbbc3-ae0b-4705-859e-3fcf2f10fdad",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Parsing: parse, tokenize and read_from_tokens\n",
|
||||||
|
"\n",
|
||||||
|
"How do we get from a sequence of characters to the abstract syntax tree that `eval` expects? The function `parse` does the job, in two steps: \n",
|
||||||
|
"1. **Lexical analysis**: the function `tokenize` breaks the characters into tokens (such as the keyword `\"begin\"` or the number `\"10\"`).\n",
|
||||||
|
"2. **Syntactic analysis**: the function `read_from_tokens` converts the tokens into an expression.\n",
|
||||||
|
"\n",
|
||||||
|
"There are many tools for lexical analysis (such as Mike Lesk and Eric Schmidt's [lex](https://en.wikipedia.org/wiki/Lex_(software%29)), but we will use a very simple tool: Python's `str.split`: "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 3,
|
||||||
|
"id": "48875a7a-3c86-4322-9307-3592ab327924",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def tokenize(chars: str) -> list:\n",
|
||||||
|
" \"\"\"Convert a string of characters into a list of tokens.\n",
|
||||||
|
" (Put spaces around parens, then split on spaces.)\"\"\"\n",
|
||||||
|
" return chars.replace('(', ' ( ').replace(')', ' ) ').split()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "64d2de32-6e7f-43ba-b6db-eb1eff397ff2",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"`read_from_tokens` looks at the first token; if it is a `)` that's a syntax error. If it is a `(`, then we start building up a list of sub-expressions until we hit a matching ')'. Any non-parenthesis token must be an atom. First try to interpret it as a number, and failing that, it must be a symbol. "
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 4,
|
||||||
|
"id": "c2677c70-f08f-49ff-b167-0cb492ec124f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def read_from_tokens(tokens: list[str]) -> Exp:\n",
|
||||||
|
" \"\"\"Read an expression from a list of tokens, mutating the list.\"\"\"\n",
|
||||||
|
" token = tokens.pop(0) if tokens else None\n",
|
||||||
|
" match token:\n",
|
||||||
|
" case None:\n",
|
||||||
|
" raise SyntaxError('unexpected end of expression')\n",
|
||||||
|
" case ')':\n",
|
||||||
|
" raise SyntaxError('unexpected \")\"')\n",
|
||||||
|
" case '(':\n",
|
||||||
|
" result = []\n",
|
||||||
|
" while tokens[0] != ')':\n",
|
||||||
|
" result.append(read_from_tokens(tokens))\n",
|
||||||
|
" tokens.pop(0) # pop off ')'\n",
|
||||||
|
" return result\n",
|
||||||
|
" case _:\n",
|
||||||
|
" try:\n",
|
||||||
|
" n = float(token)\n",
|
||||||
|
" return int(n) if n.is_integer() else n\n",
|
||||||
|
" except ValueError:\n",
|
||||||
|
" return token # symbol"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "99ae3c30-3d69-4f26-a835-f4ad44e4265b",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Now we're ready to parse a sample program:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 5,
|
||||||
|
"id": "69509c3a-2c1e-435e-8563-9a9a82cfc75f",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"['begin', ['define', 'r', 10], ['*', 'pi', ['*', 'r', 'r']]]"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 5,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"def parse(program: str) -> Exp:\n",
|
||||||
|
" \"\"\"Read a Scheme expression from a string.\n",
|
||||||
|
" (First split the program string into tokens, then read from the token list.\"\"\"\n",
|
||||||
|
" return read_from_tokens(tokenize(program))\n",
|
||||||
|
"\n",
|
||||||
|
"program = \"(begin (define r 10) (* pi (* r r)))\"\n",
|
||||||
|
"parse(program)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3ec8b0e9-e384-48fa-8539-2794114f2275",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"We can also see the tokenization of the program:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 6,
|
||||||
|
"id": "75485bb1-9625-470e-bfa2-3e3c39cb43c0",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"['(',\n",
|
||||||
|
" 'begin',\n",
|
||||||
|
" '(',\n",
|
||||||
|
" 'define',\n",
|
||||||
|
" 'r',\n",
|
||||||
|
" '10',\n",
|
||||||
|
" ')',\n",
|
||||||
|
" '(',\n",
|
||||||
|
" '*',\n",
|
||||||
|
" 'pi',\n",
|
||||||
|
" '(',\n",
|
||||||
|
" '*',\n",
|
||||||
|
" 'r',\n",
|
||||||
|
" 'r',\n",
|
||||||
|
" ')',\n",
|
||||||
|
" ')',\n",
|
||||||
|
" ')']"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 6,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"tokenize(program)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "e9ae14da-9668-405a-8c5c-2a7186e98cb4",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Environments\n",
|
||||||
|
"\n",
|
||||||
|
"We mentioned in passing that an **environment** is a mapping from variable names to their values. We will define a default global environment with the names for a bunch of standard functions (like `sqrt` and `max`, and also operators like `+` and `>`). This environment can be augmented with user-defined variables, using the expression `(define symbol value)`."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 7,
|
||||||
|
"id": "ce21a511-1089-4d70-9c9a-8825c3d63b17",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"global_env = Env({\n",
|
||||||
|
" '+':op.add, '-':op.sub, '*':op.mul, '/':op.truediv, \n",
|
||||||
|
" '>':op.gt, '<':op.lt, '>=':op.ge, '<=':op.le, '=':op.eq, \n",
|
||||||
|
" 'abs': abs,\n",
|
||||||
|
" 'append': op.add, \n",
|
||||||
|
" 'apply': lambda proc, args: proc(*args),\n",
|
||||||
|
" 'begin': lambda *x: x[-1],\n",
|
||||||
|
" 'car': lambda x: x[0],\n",
|
||||||
|
" 'cdr': lambda x: x[1:], \n",
|
||||||
|
" 'cons': lambda x,y: [x] + y,\n",
|
||||||
|
" 'eq?': op.is_, \n",
|
||||||
|
" 'equal?': op.eq,\n",
|
||||||
|
" 'expt': pow,\n",
|
||||||
|
" 'length': len, \n",
|
||||||
|
" 'list': lambda *x: List(x), \n",
|
||||||
|
" 'list?': lambda x: isinstance(x, list), \n",
|
||||||
|
" 'map': map,\n",
|
||||||
|
" 'max': max, 'min': min,\n",
|
||||||
|
" 'not': op.not_,\n",
|
||||||
|
" 'null?': lambda x: x == [], \n",
|
||||||
|
" 'number?': lambda x: isinstance(x, Number), \n",
|
||||||
|
"\t'print': print,\n",
|
||||||
|
" 'procedure?': callable,\n",
|
||||||
|
" 'round': round,\n",
|
||||||
|
" 'symbol?': lambda x: isinstance(x, Symbol),\n",
|
||||||
|
" **vars(math)})"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "565a02c7-15b1-4965-a60e-240464a10ed4",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"We're done! You can see it all in action:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 8,
|
||||||
|
"id": "d09b539c-39d9-481d-9e5e-64ca3b16fbda",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"314.1592653589793"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 8,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"eval(parse(program), global_env)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "ffe3866c-76ee-4a6e-b0d2-efb18b544c1e",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## Interaction: A REPL\n",
|
||||||
|
"\n",
|
||||||
|
"It is tedious to have to enter `eval(parse(\"...\", global_env))` all the time. One of Lisp's great legacies is the notion of an interactive read-eval-print loop: a way for a programmer to enter an expression, and see it immediately read, evaluated, and printed, without having to go through a lengthy build/compile/run cycle. So let's define the function `repl` (which stands for read-eval-print-loop), and the function `scheme_to_str` which returns a string representing a Scheme object."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 9,
|
||||||
|
"id": "037afa47-c35e-416f-affb-aa219cfff935",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def repl(prompt='\\nlis.py> '):\n",
|
||||||
|
" \"\"\"A prompt-read-eval-print loop.\"\"\"\n",
|
||||||
|
" print('Scheme read-eval-print loop. Type exit to exit')\n",
|
||||||
|
" while (expr := parse(input(prompt))) != 'exit':\n",
|
||||||
|
" val = eval(expr, global_env)\n",
|
||||||
|
" if val is not None: \n",
|
||||||
|
" print(scheme_to_str(val))\n",
|
||||||
|
"\n",
|
||||||
|
"def scheme_to_str(exp):\n",
|
||||||
|
" \"Convert a Python object back into a Scheme-readable string.\"\n",
|
||||||
|
" if isinstance(exp, List):\n",
|
||||||
|
" return '(' + ' '.join(map(scheme_to_str, exp)) + ')' \n",
|
||||||
|
" else:\n",
|
||||||
|
" return str(exp)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "113b7499-db18-4320-ad82-af66364228f3",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Here is `repl` in action. You can run it yourself in a cell.\n",
|
||||||
|
"\n",
|
||||||
|
" >>> repl()\n",
|
||||||
|
" lis.py> (define r 10)\n",
|
||||||
|
" lis.py> (* pi (* r r))\n",
|
||||||
|
" 314.159265359\n",
|
||||||
|
" lis.py> (if (> (* 11 11) 120) (* 7 6) oops)\n",
|
||||||
|
" 42\n",
|
||||||
|
" lis.py> (list (+ 1 1) (+ 2 2) (* 2 3) (expt 2 3))\n",
|
||||||
|
" lis.py> (2 4 6 8)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "ed98aea1-201a-4532-9cf4-56aff0ba74e6",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"# Language 2: Full Lispy\n",
|
||||||
|
"\n",
|
||||||
|
"We will now extend our language with three new special forms, giving us a much more nearly-complete Scheme subset:\n",
|
||||||
|
"\n",
|
||||||
|
"|Expression\t|Syntax| Example|\tSemantics|\n",
|
||||||
|
"|-----------|------|--------|------------|\n",
|
||||||
|
"|quotation\t|`(quote` *exp*`)`| `(quote (+ 1 2))` ⇒ `(+ 1 2)`|\tReturn the exp literally; do not evaluate it.|\n",
|
||||||
|
"|assignment\t|`(set!` *symbol exp*`)`| `(set! r2 (* r r))`|\tEvaluate *exp* and assign that value to *symbol*.|\n",
|
||||||
|
"|procedure\t|`(lambda (`*symbol...*`)` *exp*`)`|`(lambda (r) (* pi (* r r)))`|\tCreate a procedure with parameter(s) named *symbol...* and *exp* as the body.|\n",
|
||||||
|
"\n",
|
||||||
|
"The lambda special form (an obscure nomenclature choice that refers to Alonzo Church's [lambda calculus](https://en.wikipedia.org/wiki/Lambda_calculus)) creates a procedure. We want procedures to work like this:\n",
|
||||||
|
"\n",
|
||||||
|
" lis.py> (define circle-area (lambda (r) (* pi (* r r)))\n",
|
||||||
|
" lis.py> (circle-area (+ 5 5))\n",
|
||||||
|
" 314.159265359\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"There are two steps here. In the first step, the lambda expression is evaluated to create a procedure, one which refers to the global variables pi and `*`, and takes a single parameter, which it calls `r`. This procedure is defined as the value of the new variable `circle-area`. In the second step, the procedure we just defined is the value of circle-area, so it is called, with the value 10 as the argument. We want `r` to take on the value 10, but it wouldn't do to just set `r` to 10 in the global environment. What if we were using `r` for some other purpose? We wouldn't want a call to `circle-area` to alter that value. Instead, we want to arrange for there to be a **local variable** named `r `that we can set to 10 without worrying about interfering with any global variable that happens to have the same name. The process for calling a procedure introduces these new local variable(s), binding each symbol in the parameter list of the function to the corresponding value in the argument list of the function call.\n",
|
||||||
|
"\n",
|
||||||
|
"The difference between `set!` and `define` is that `set!` always refers to a previously-defined variable, whcih might be in the innermost environment, or might be in an outer environment. `define` always introduces a new variable in the innermost environment.\n",
|
||||||
|
"\n",
|
||||||
|
"\n",
|
||||||
|
"## Redefining Env as a Class\n",
|
||||||
|
"\n",
|
||||||
|
"To handle local variables, we will redefine `Env` to be a subclass of `dict.` When we evaluate (`circle-area (+ 5 5))`, we will fetch the procedure body, `(* pi (* r r))`, and evaluate it in an environment that has `r` as the sole local variable (with value 10), but also has the global environment as the \"outer\" environment; it is there that we will find the values of `*` and `pi`. In the diagram, the inner environment is blue and the outer red:\n",
|
||||||
|
"\n",
|
||||||
|
"<p><table style=\"border: 3px solid red\" cellspacing=1 cellpadding=5><tr><td>\n",
|
||||||
|
"<tt>pi: 3.141592653589793\n",
|
||||||
|
"<br>*: <built-in function mul>\n",
|
||||||
|
"<br>...\n",
|
||||||
|
"<br>\n",
|
||||||
|
"<table style=\"border: 3px solid blue\" cellspacing=1 cellpadding=5>\n",
|
||||||
|
"<tr><td>r: 10\n",
|
||||||
|
"</table>\n",
|
||||||
|
"</table>\n",
|
||||||
|
"\n",
|
||||||
|
"When we look up a variable in such a nested environment, we look first at the innermost level, but if we don't find the variable name there, we move to the next outer level. Procedures and environments are intertwined, so let's define them together:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 10,
|
||||||
|
"id": "ed27933e-dc95-4e75-a021-853d8a60d231",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"from dataclasses import dataclass\n",
|
||||||
|
"\n",
|
||||||
|
"class Env(dict):\n",
|
||||||
|
" \"\"\"An environment: a dict of {'var': val} pairs, with an outer Env.\"\"\"\n",
|
||||||
|
" def __init__(self, inner, outer=None):\n",
|
||||||
|
" self.update(inner)\n",
|
||||||
|
" self.outer = outer\n",
|
||||||
|
" def find(self, var):\n",
|
||||||
|
" \"\"\"Find the innermost Env where var appears.\"\"\"\n",
|
||||||
|
" return self if (var in self or not self.outer) else self.outer.find(var)\n",
|
||||||
|
"\n",
|
||||||
|
"@dataclass\n",
|
||||||
|
"class Procedure(object):\n",
|
||||||
|
" \"\"\"A user-defined Scheme procedure.\"\"\"\n",
|
||||||
|
" parms: list[Symbol]\n",
|
||||||
|
" body: Exp\n",
|
||||||
|
" env: Env\n",
|
||||||
|
" def __call__(self, *args): \n",
|
||||||
|
" return eval(self.body, Env(zip(self.parms, args), outer=self.env))\n",
|
||||||
|
"\n",
|
||||||
|
"# Now transfer the old global_env into a new Env\n",
|
||||||
|
"global_env = Env(global_env)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "3d0599c6-3a35-4fd8-b179-c970e8a36c4d",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"We see that every procedure has three components: a list of parameter names, a body expression, and an environment that tells us what other variables are accessible from the body. For a procedure defined at the top level this will be the global environment, but it is also possible for a procedure to refer to the local variables of the environment in which it was defined (**not** the environment in which it is called).\n",
|
||||||
|
"\n",
|
||||||
|
"An environment is a subclass of `dict`, so it has all the methods that `dict` has. In addition there are two methods: the constructor `__init__` builds a new environment by taking a list of parameter names and a corresponding list of argument values, and creating a new environment that has those {variable: value} pairs as the inner part, and also refers to the given outer environment. The method `find` is used to find the right environment for a variable: starting with the inner one and going out, find the first environment that mentions the variable name.\n",
|
||||||
|
"\n",
|
||||||
|
"To see how these all go together, here is the new definition of `eval`. Note that the clause for variable reference has changed: we now have to call `env.find(exp)` to find at what level the variable exists; then we can fetch the value of the variable from that level. (The clause for `define` has not changed, because a define always adds a new variable to the innermost environment.) There are three new clauses: `quote` returns the constant without evaluating it, `set!`, finds the environment level where the variable exists and sets it to a new value there, and for `lambda` we create a new procedure object with the given parameter list, body, and environment."
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 11,
|
||||||
|
"id": "afa69b35-bac3-459c-a0cc-b94656ab474c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"def eval(exp: Exp, env=global_env) -> object:\n",
|
||||||
|
" \"\"\"Evaluate an expression in an environment.\"\"\"\n",
|
||||||
|
" match exp:\n",
|
||||||
|
" case Symbol(): # variable reference\n",
|
||||||
|
" return env.find(exp)[exp]\n",
|
||||||
|
" case Number(): # constant \n",
|
||||||
|
" return exp\n",
|
||||||
|
" case ('if', test, then, els): # conditional evaluates one branch or the other\n",
|
||||||
|
" branch = (then if eval(test, env) \n",
|
||||||
|
" else els)\n",
|
||||||
|
" return eval(branch, env)\n",
|
||||||
|
" case ('define', name, exp): # definition\n",
|
||||||
|
" env[name] = eval(exp, env)\n",
|
||||||
|
" case ('quote', constant):\n",
|
||||||
|
" return constant\n",
|
||||||
|
" case ('set!', symbol, exp):\n",
|
||||||
|
" env.find(symbol)[symbol] = eval(exp, env)\n",
|
||||||
|
" case ('lambda', parms, body):\n",
|
||||||
|
" return Procedure(parms, body, env)\n",
|
||||||
|
" case (proc, *args): # procedure call\n",
|
||||||
|
" func = eval(proc, env)\n",
|
||||||
|
" vals = [eval(arg, env) for arg in args]\n",
|
||||||
|
" return func(*vals)"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "499ec617-e3f5-4f5e-9e8e-e7da7656c624",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"You can run try new version by removing the `#` and running the cell below:"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 12,
|
||||||
|
"id": "8a76bb61-7195-4b1d-8b01-fb6a5825b94c",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [],
|
||||||
|
"source": [
|
||||||
|
"# repl()"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "1a2ccfcd-326b-44f0-b041-da657dc042a0",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"Here is an example of what you can do:\n",
|
||||||
|
"\n",
|
||||||
|
" >>> repl()\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define circle-area (lambda (r) (* pi (* r r))))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (circle-area 3)\n",
|
||||||
|
" 28.274333877\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define fact (lambda (n) (if (<= n 1) 1 (* n (fact (- n 1))))))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (fact 10)\n",
|
||||||
|
" 3628800\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (fact 100)\n",
|
||||||
|
" 9332621544394415268169923885626670049071596826438162146859296389521759999322991\n",
|
||||||
|
" 5608941463976156518286253697920827223758251185210916864000000000000000000000000\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (circle-area (fact 10))\n",
|
||||||
|
" 4.1369087198e+13\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define first car)\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define rest cdr)\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define count (lambda (item L) (if L (+ (equal? item (first L)) (count item (rest L))) 0)))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (count 0 (list 0 1 2 3 0 0))\n",
|
||||||
|
" 3\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (count (quote the) (quote (the more the merrier the bigger the better)))\n",
|
||||||
|
" 4\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define twice (lambda (x) (* 2 x)))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (twice 5)\n",
|
||||||
|
" 10\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define repeat (lambda (f) (lambda (x) (f (f x)))))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> ((repeat twice) 10)\n",
|
||||||
|
" 40\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> ((repeat (repeat twice)) 10)\n",
|
||||||
|
" 160\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> ((repeat (repeat (repeat twice))) 10)\n",
|
||||||
|
" 2560\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> ((repeat (repeat (repeat (repeat twice)))) 10)\n",
|
||||||
|
" 655360\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (pow 2 16)\n",
|
||||||
|
" 65536.0\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define fib (lambda (n) (if (< n 2) 1 (+ (fib (- n 1)) (fib (- n 2))))))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (define range (lambda (a b) (if (= a b) (quote ()) (cons a (range (+ a 1) b)))))\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (range 0 10)\n",
|
||||||
|
" (0 1 2 3 4 5 6 7 8 9)\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (map fib (range 0 10))\n",
|
||||||
|
" (1 1 2 3 5 8 13 21 34 55)\n",
|
||||||
|
" \n",
|
||||||
|
" lis.py> (map fib (range 0 20))\n",
|
||||||
|
" (1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765)\n",
|
||||||
|
" \n",
|
||||||
|
" \n",
|
||||||
|
"We now have a language with procedures, variables, conditionals (`if`), and sequential execution (`begin`). If you are familiar with other languages, you might think that a while or for loop would be needed, but Scheme manages to do without these just fine. The Scheme report says \"Scheme demonstrates that a very small number of rules for forming expressions, with no restrictions on how they are composed, suffice to form a practical and efficient programming language.\" In Scheme you iterate by defining recursive functions.\n",
|
||||||
|
"\n",
|
||||||
|
"## How Small/Complete/Good/Fast is Lispy?\n",
|
||||||
|
" \n",
|
||||||
|
"In which we judge Lispy on several criteria:\n",
|
||||||
|
"- **Small**: Lispy is *very* small: about 120 lines or 4K of source code. (An earlier version was just 90 lines, but was perhaps a bit too terse.) The smallest version of my Scheme in Java, [Jscheme](http://norvig.com/jscheme.html) was 1664 lines and 57K of source. Jscheme was\n",
|
||||||
|
" originally called SILK (Scheme in Fifty Kilobytes), but I only kept\n",
|
||||||
|
" under that limit by counting bytecode rather than source code. Lispy does much\n",
|
||||||
|
" better; I think it meets Alan Kay's 1972 [claim](http://gagne.homedns.org/~tgagne/contrib/EarlyHistoryST.html)\n",
|
||||||
|
" that *you could define the \"most powerful language in the world\" in \"a page of code.\"* (if you use a small font). However,\n",
|
||||||
|
" I think Alan would disagree, because he would count the Python compiler as part of the code, putting me <i>well</i> over a page.\n",
|
||||||
|
"- **Complete**: Lispy is not very complete compared to the Scheme standard. Some major shortcomings:\n",
|
||||||
|
" - **Syntax**: Missing comments, quote and quasiquote notation, # literals, the derived\n",
|
||||||
|
" expression types (such as `cond`, derived from `if`,\n",
|
||||||
|
" or `let`, derived from `lambda`), and dotted list notation.\n",
|
||||||
|
" - **Semantics**: Missing `call/cc` and tail recursion. \n",
|
||||||
|
" - **Data Types**: Missing strings, characters, booleans, ports,\n",
|
||||||
|
" vectors, exact/inexact numbers. A Scheme list should actually be a custom data class, not a Python list.\n",
|
||||||
|
" - **Procedures**: Missing over 100 primitive procedures.\n",
|
||||||
|
" - **Error recovery**: Lispy does not attempt to detect,\n",
|
||||||
|
" reasonably report, or recover from errors. Lispy expects the\n",
|
||||||
|
" programmer to be perfect. (That is an environment issue, not a language issue.)\n",
|
||||||
|
"- **Good**: That's up to the readers to decide. I think that Lispy is good for my purpose of explaining Lisp interpreters.\n",
|
||||||
|
"- **Fast**: Lispy computes <tt>(fact 100)</tt> in about a millisecond. That's fast enough for me (although slower than some\n",
|
||||||
|
"other ways of computing it). <p>"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "code",
|
||||||
|
"execution_count": 13,
|
||||||
|
"id": "ececc132-11f2-4f76-80d4-771b89f93f14",
|
||||||
|
"metadata": {},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "stdout",
|
||||||
|
"output_type": "stream",
|
||||||
|
"text": [
|
||||||
|
"CPU times: user 371 μs, sys: 108 μs, total: 479 μs\n",
|
||||||
|
"Wall time: 484 μs\n"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"data": {
|
||||||
|
"text/plain": [
|
||||||
|
"93326215443944152681699238856266700490715968264381621468592963895217599993229915608941463976156518286253697920827223758251185210916864000000000000000000000000"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"execution_count": 13,
|
||||||
|
"metadata": {},
|
||||||
|
"output_type": "execute_result"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": [
|
||||||
|
"fact = \"\"\"(begin (define fact (lambda (n) \n",
|
||||||
|
" (if (<= n 1) \n",
|
||||||
|
" 1 \n",
|
||||||
|
" (* n (fact (- n 1)))))) \n",
|
||||||
|
" (fact 100))\"\"\"\n",
|
||||||
|
"\n",
|
||||||
|
"%time eval(parse(fact))"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"cell_type": "markdown",
|
||||||
|
"id": "5f7fcfb8-ef93-4dfb-af8b-43030ab74eaa",
|
||||||
|
"metadata": {},
|
||||||
|
"source": [
|
||||||
|
"## True Story\n",
|
||||||
|
"\n",
|
||||||
|
"To back up the idea that it can be very helpful to know how\n",
|
||||||
|
"interpreters work, here's a story. Way back in 1984 I was writing a\n",
|
||||||
|
"Ph.D. thesis. This was before LaTeX, before Microsoft Word for Windows–we used\n",
|
||||||
|
"[troff](https://en.wikipedia.org/wiki/Troff). Unfortunately, troff had no facility for forward references\n",
|
||||||
|
"to symbolic labels: I wanted to be able to write \"As we will see on\n",
|
||||||
|
"page @theorem-x\" and then write something like \"@(set theorem-x \\n%)\" in\n",
|
||||||
|
"the appropriate place (the troff register \\n% holds the page number). My\n",
|
||||||
|
"fellow grad student Tony DeRose felt the same need, and together we\n",
|
||||||
|
"sketched out a simple Lisp program that would handle this as a preprocessor. However,\n",
|
||||||
|
"it turned out that the Lisp we had at the time was good at reading\n",
|
||||||
|
"Lisp expressions, but slow at reading 100 KB of characters one character at a time.\n",
|
||||||
|
"\n",
|
||||||
|
"From there Tony and I split paths. He reasoned that the hard part was\n",
|
||||||
|
"the interpreter for expressions; he needed Lisp for that, but he knew\n",
|
||||||
|
"how to write a tiny C routine\n",
|
||||||
|
"for reading the characters one at a time, and how to link the C routine into the Lisp\n",
|
||||||
|
"program. I didn't know how to do that linking, but I reasoned that writing an\n",
|
||||||
|
"interpreter for this trivial language (all it had was set variable,\n",
|
||||||
|
"fetch variable, and string concatenate) was easy, so I wrote an\n",
|
||||||
|
"interpreter in C. So, ironically, Tony wrote a Lisp program (with one small routine in C) because he was a\n",
|
||||||
|
"C programmer, and I wrote a C program (that implements a hand-coded mini-interpreter) because I was a Lisp programmer.\n",
|
||||||
|
"\n",
|
||||||
|
"In the end, we both got our theses done (<a href=\"http://www.eecs.berkeley.edu/Pubs/TechRpts/1985/6081.html\">Tony</a>, <a href=\"http://www.eecs.berkeley.edu/Pubs/TechRpts/1987/5995.html\">Peter</a>).\n",
|
||||||
|
"\n",
|
||||||
|
"<h2>Further Reading</h2>\n",
|
||||||
|
"\n",
|
||||||
|
" \n",
|
||||||
|
" \n",
|
||||||
|
" <p>To learn more about Scheme consult some of the fine books (by\n",
|
||||||
|
" <a\n",
|
||||||
|
" href=\"http://books.google.com/books?id=xyO-KLexVnMC&lpg=PP1&dq=scheme%20programming%20book&pg=PP1#v=onepage&q&f=false\">Friedman\n",
|
||||||
|
" and Fellesein</a>,\n",
|
||||||
|
" <a href=\"http://books.google.com/books?id=wftS4tj4XFMC&lpg=PA300&dq=scheme%20programming%20book&pg=PP1#v=onepage&q&f=false\">Dybvig</a>,\n",
|
||||||
|
" <a\n",
|
||||||
|
" href=\"http://books.google.com/books?id=81mFK8pqh5EC&lpg=PP1&dq=scheme%20programming%20book&pg=PP1#v=onepage&q&f=false\">Queinnec</a>,\n",
|
||||||
|
" <a href=\"http://www.eecs.berkeley.edu/~bh/ss-toc2.html\">Harvey and\n",
|
||||||
|
" Wright</a> or\n",
|
||||||
|
" <a href=\"https://www.researchgate.net/profile/Gerald-Sussman-2/publication/37597721_Structure_and_Interpretation_of_Computer_Programs_H_Abelson_GJ_Sussman_colaboracion_de_J_Sussman_prol_de_Alan_J_Perlis/links/53d141450cf220632f392bf7/Structure-and-Interpretation-of-Computer-Programs-H-Abelson-GJ-Sussman-colaboracion-de-J-Sussman-prol-de-Alan-J-Perlis.pdf\">Sussman and Abelson</a>),\n",
|
||||||
|
" videos (by <a\n",
|
||||||
|
" href=\"http://groups.csail.mit.edu/mac/classes/6.001/abelson-sussman-lectures/\">Abelson\n",
|
||||||
|
" and Sussman</a>),\n",
|
||||||
|
" tutorials (by\n",
|
||||||
|
" <a\n",
|
||||||
|
" href=\"http://www.ccs.neu.edu/home/dorai/t-y-scheme/t-y-scheme.html\">Dorai</a>,\n",
|
||||||
|
" <a href=\"http://docs.racket-lang.org/quick/index.html\">PLT</a>, or\n",
|
||||||
|
" <a href=\"http://cs.gettysburg.edu/~tneller/cs341/scheme-intro/index.html\">Neller</a>),\n",
|
||||||
|
" or the\n",
|
||||||
|
" <a\n",
|
||||||
|
" href=\"http://www.schemers.org/Documents/Standards/R5RS/HTML\">reference\n",
|
||||||
|
" manual</a>.\n",
|
||||||
|
"\n",
|
||||||
|
"<p>I also have another page describing a <a href=\"http://norvig.com/lispy2.html\">more advanced version of Lispy</a>."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadata": {
|
||||||
|
"kernelspec": {
|
||||||
|
"display_name": "Python 3 (ipykernel)",
|
||||||
|
"language": "python",
|
||||||
|
"name": "python3"
|
||||||
|
},
|
||||||
|
"language_info": {
|
||||||
|
"codemirror_mode": {
|
||||||
|
"name": "ipython",
|
||||||
|
"version": 3
|
||||||
|
},
|
||||||
|
"file_extension": ".py",
|
||||||
|
"mimetype": "text/x-python",
|
||||||
|
"name": "python",
|
||||||
|
"nbconvert_exporter": "python",
|
||||||
|
"pygments_lexer": "ipython3",
|
||||||
|
"version": "3.13.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nbformat": 4,
|
||||||
|
"nbformat_minor": 5
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user