Made commands more powerful, added :load and :save
This commit is contained in:
		| @ -2,7 +2,6 @@ | ||||
|  | ||||
|  | ||||
| ## Todo (pre-release): | ||||
|  - Good command parsing (`:save`, `:load`, are a bare minimum) | ||||
|  - $\alpha$-equivalence check | ||||
|  - Prettyprint functions (combine args, rename bound variables) | ||||
|  - Write a nice README | ||||
| @ -18,6 +17,7 @@ | ||||
|  - Syntax highlighting: parenthesis, bound variables, macros, etc | ||||
|  - Pin header to top of screen | ||||
|  - PyPi package | ||||
|  - Smart alignment in all printouts | ||||
|  | ||||
| ## Mention in Docs | ||||
|  - lambda functions only work with single-letter arguments | ||||
|  | ||||
| @ -8,7 +8,6 @@ from prompt_toolkit.lexers import Lexer | ||||
|  | ||||
| from pyparsing import exceptions as ppx | ||||
|  | ||||
| from lamb.parser import Parser | ||||
| import lamb.runner as runner | ||||
| import lamb.runstatus as rs | ||||
| import lamb.tokens as tokens | ||||
| @ -23,27 +22,29 @@ class LambdaLexer(Lexer): | ||||
| 			return [("class:text", str(document.lines[line_no]))] | ||||
| 		return inner | ||||
|  | ||||
| # Replace "\" with a pretty "λ" in the prompt | ||||
|  | ||||
| utils.show_greeting() | ||||
|  | ||||
|  | ||||
| # Replace "\" with pretty "λ"s | ||||
| bindings = KeyBindings() | ||||
| @bindings.add("\\") | ||||
| def _(event): | ||||
| 	event.current_buffer.insert_text("λ") | ||||
|  | ||||
| session = PromptSession( | ||||
| 	message = FormattedText([ | ||||
| 		("class:prompt", "~~> ") | ||||
| 	]), | ||||
|  | ||||
| r = runner.Runner( | ||||
| 	prompt_session = PromptSession( | ||||
| 		style = utils.style, | ||||
| 		lexer = LambdaLexer(), | ||||
| 		key_bindings = bindings | ||||
| 	), | ||||
|  | ||||
| 	prompt_message = FormattedText([ | ||||
| 		("class:prompt", "~~> ") | ||||
| 	]), | ||||
| ) | ||||
|  | ||||
|  | ||||
| utils.show_greeting() | ||||
|  | ||||
|  | ||||
| r = runner.Runner() | ||||
|  | ||||
| r.run_lines([ | ||||
| 	"T = λab.a", | ||||
| 	"F = λab.b", | ||||
| @ -63,7 +64,7 @@ r.run_lines([ | ||||
|  | ||||
| while True: | ||||
| 	try: | ||||
| 		i = session.prompt() | ||||
| 		i = r.prompt() | ||||
|  | ||||
| 	# Catch Ctrl-C and Ctrl-D | ||||
| 	except KeyboardInterrupt: | ||||
| @ -83,7 +84,7 @@ while True: | ||||
| 	try: | ||||
| 		x = r.run(i) | ||||
| 	except ppx.ParseException as e: | ||||
| 		l = len(to_plain_text(session.message)) | ||||
| 		l = len(to_plain_text(r.prompt_session.message)) | ||||
| 		printf(FormattedText([ | ||||
| 			("class:err", " "*(e.loc + l) + "^\n"), | ||||
| 			("class:err", f"Syntax error at char {e.loc}."), | ||||
| @ -107,7 +108,7 @@ while True: | ||||
|  | ||||
|  | ||||
| 	if isinstance(x, rs.CommandStatus): | ||||
| 		printf(x.formatted_text, style = utils.style) | ||||
| 		pass | ||||
|  | ||||
| 	# If this line was an expression, print reduction status | ||||
| 	elif isinstance(x, rs.ReduceStatus): | ||||
|  | ||||
							
								
								
									
										162
									
								
								lamb/commands.py
									
									
									
									
									
								
							
							
						
						
									
										162
									
								
								lamb/commands.py
									
									
									
									
									
								
							| @ -1,8 +1,12 @@ | ||||
| from prompt_toolkit.formatted_text import FormattedText | ||||
| from prompt_toolkit.formatted_text import HTML | ||||
| from prompt_toolkit import print_formatted_text as printf | ||||
| from prompt_toolkit.shortcuts import clear as clear_screen | ||||
|  | ||||
| from lamb.runstatus import CommandStatus | ||||
| import os.path | ||||
|  | ||||
| from pyparsing import exceptions as ppx | ||||
| import lamb.runstatus as rs | ||||
| import lamb.utils as utils | ||||
|  | ||||
|  | ||||
| @ -16,60 +20,165 @@ def lamb_command(*, help_text: str): | ||||
| 		help_texts[func.__name__] = help_text | ||||
| 	return inner | ||||
|  | ||||
| def run(command, runner): | ||||
| def run(command, runner) -> None: | ||||
| 	if command.name not in commands: | ||||
| 		return CommandStatus( | ||||
| 			formatted_text = FormattedText([ | ||||
| 		printf( | ||||
| 			FormattedText([ | ||||
| 				("class:warn", f"Unknown command \"{command.name}\"") | ||||
| 			]) | ||||
| 			]), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 	else: | ||||
| 		return commands[command.name](command, runner) | ||||
| 		commands[command.name](command, runner) | ||||
|  | ||||
|  | ||||
| @lamb_command(help_text = "Save macros to a file") | ||||
| def save(command, runner) -> None: | ||||
| 	if len(command.args) != 1: | ||||
| 		printf( | ||||
| 			HTML( | ||||
| 				"<err>Command <cmd_code>:save</cmd_code> takes exactly one argument.</err>" | ||||
| 			), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 		return | ||||
|  | ||||
| 	target = command.args[0] | ||||
| 	if os.path.exists(target): | ||||
| 		confirm = runner.prompt_session.prompt( | ||||
| 			message = FormattedText([ | ||||
| 				("class:warn", "File exists. Overwrite? "), | ||||
| 				("class:text", "[yes/no]: ") | ||||
| 			]) | ||||
| 		).lower() | ||||
|  | ||||
| 		if confirm != "yes": | ||||
| 			printf( | ||||
| 				HTML( | ||||
| 					"<err>Cancelled.</err>" | ||||
| 				), | ||||
| 				style = utils.style | ||||
| 			) | ||||
| 			return | ||||
|  | ||||
| 	with open(target, "w") as f: | ||||
| 		f.write("\n".join( | ||||
| 			[f"{n} = {e}" for n, e in runner.macro_table.items()] | ||||
| 		)) | ||||
|  | ||||
| 	printf( | ||||
| 		HTML( | ||||
| 			f"Wrote {len(runner.macro_table)} macros to <cmd_code>{target}</cmd_code>" | ||||
| 		), | ||||
| 		style = utils.style | ||||
| 	) | ||||
|  | ||||
|  | ||||
| @lamb_command(help_text = "Load macros from a file") | ||||
| def load(command, runner): | ||||
| 	if len(command.args) != 1: | ||||
| 		printf( | ||||
| 			HTML( | ||||
| 				"<err>Command <cmd_code>:load</cmd_code> takes exactly one argument.</err>" | ||||
| 			), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 		return | ||||
|  | ||||
| 	target = command.args[0] | ||||
| 	if not os.path.exists(target): | ||||
| 		printf( | ||||
| 			HTML( | ||||
| 				f"<err>File {target} doesn't exist.</err>" | ||||
| 			), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 		return | ||||
|  | ||||
| 	with open(target, "r") as f: | ||||
| 		lines = [x.strip() for x in f.readlines()] | ||||
|  | ||||
| 	for i in range(len(lines)): | ||||
| 		l = lines[i] | ||||
| 		try: | ||||
| 			x = runner.run(l, macro_only = True) | ||||
| 		except ppx.ParseException as e: | ||||
| 			printf( | ||||
| 				FormattedText([ | ||||
| 					("class:warn", f"Syntax error on line {i+1:02}: "), | ||||
| 					("class:cmd_code", l[:e.loc]), | ||||
| 					("class:err", l[e.loc]), | ||||
| 					("class:cmd_code", l[e.loc+1:]) | ||||
| 				]), | ||||
| 				style = utils.style | ||||
| 			) | ||||
| 		except rs.NotAMacro: | ||||
| 			printf( | ||||
| 				FormattedText([ | ||||
| 					("class:warn", f"Skipping line {i+1:02}: "), | ||||
| 					("class:cmd_code", l), | ||||
| 					("class:warn", f" is not a macro definition.") | ||||
| 				]), | ||||
| 				style = utils.style | ||||
| 			) | ||||
| 		else: | ||||
| 			printf( | ||||
| 				FormattedText([ | ||||
| 					("class:ok", f"Loaded {x.macro_label}: "), | ||||
| 					("class:cmd_code", str(x.macro_expr)) | ||||
| 				]), | ||||
| 				style = utils.style | ||||
| 			) | ||||
|  | ||||
|  | ||||
|  | ||||
| @lamb_command(help_text = "Delete a macro") | ||||
| def mdel(command, runner): | ||||
| def mdel(command, runner) -> None: | ||||
| 	if len(command.args) != 1: | ||||
| 		return CommandStatus( | ||||
| 			formatted_text = HTML( | ||||
| 				"<warn>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</warn>" | ||||
| 			) | ||||
| 		printf( | ||||
| 			HTML( | ||||
| 				"<err>Command <cmd_code>:mdel</cmd_code> takes exactly one argument.</err>" | ||||
| 			), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 		return | ||||
|  | ||||
| 	target = command.args[0] | ||||
| 	if target not in runner.macro_table: | ||||
| 		return CommandStatus( | ||||
| 			formatted_text = HTML( | ||||
| 		printf( | ||||
| 			HTML( | ||||
| 				f"<warn>Macro \"{target}\" is not defined</warn>" | ||||
| 			), | ||||
| 			style = utils.style | ||||
| 		) | ||||
| 		) | ||||
| 		return | ||||
|  | ||||
| 	del runner.macro_table[target] | ||||
|  | ||||
| @lamb_command(help_text = "Show macros") | ||||
| def macros(command, runner): | ||||
| 	return CommandStatus( | ||||
|  | ||||
| 		# Can't use HTML here, certain characters might break it. | ||||
| 		formatted_text = FormattedText([ | ||||
|  | ||||
| @lamb_command(help_text = "Show macros") | ||||
| def macros(command, runner) -> None: | ||||
| 	printf(FormattedText([ | ||||
| 			("class:cmd_h", "\nDefined Macros:\n"), | ||||
| 		] + | ||||
| 		[ | ||||
| 			("class:cmd_text", f"\t{name} \t {exp}\n") | ||||
| 			for name, exp in runner.macro_table.items() | ||||
| 		] | ||||
| 		) | ||||
| 		]), | ||||
| 		style = utils.style | ||||
| 	) | ||||
|  | ||||
| @lamb_command(help_text = "Clear the screen") | ||||
| def clear(command, runner): | ||||
| def clear(command, runner) -> None: | ||||
| 	clear_screen() | ||||
| 	utils.show_greeting() | ||||
|  | ||||
|  | ||||
| @lamb_command(help_text = "Print this help") | ||||
| def help(command, runner): | ||||
| 	return CommandStatus( | ||||
| 		formatted_text = HTML( | ||||
| def help(command, runner) -> None: | ||||
| 	printf( | ||||
| 		HTML( | ||||
| 			"\n<cmd_text>" + | ||||
| 			"<cmd_h>Usage:</cmd_h>" + | ||||
| 			"\n" + | ||||
| @ -86,5 +195,6 @@ def help(command, runner): | ||||
| 				for name, text in help_texts.items() | ||||
| 			]) + | ||||
| 			"</cmd_text>" | ||||
| 		) | ||||
| 		), | ||||
| 		style = utils.style | ||||
| 	) | ||||
| @ -1,5 +1,4 @@ | ||||
| from distutils.cmd import Command | ||||
| from prompt_toolkit.formatted_text import FormattedText | ||||
| from prompt_toolkit import PromptSession | ||||
|  | ||||
| import lamb.commands as commands | ||||
| from lamb.parser import Parser | ||||
| @ -7,15 +6,19 @@ import lamb.tokens as tokens | ||||
| import lamb.runstatus as rs | ||||
|  | ||||
|  | ||||
|  | ||||
| class Runner: | ||||
| 	def __init__(self): | ||||
| 	def __init__(self, prompt_session: PromptSession, prompt_message): | ||||
| 		self.macro_table = {} | ||||
| 		self.prompt_session = prompt_session | ||||
| 		self.prompt_message = prompt_message | ||||
|  | ||||
| 		# Maximum amount of reductions. | ||||
| 		# If None, no maximum is enforced. | ||||
| 		self.reduction_limit: int | None = 300 | ||||
|  | ||||
| 	def prompt(self): | ||||
| 		return self.prompt_session.prompt(message = self.prompt_message) | ||||
|  | ||||
|  | ||||
| 	def reduce_expression(self, expr: tokens.LambdaToken) -> rs.ReduceStatus: | ||||
|  | ||||
| @ -51,7 +54,7 @@ class Runner: | ||||
|  | ||||
|  | ||||
| 	# Apply a list of definitions | ||||
| 	def run(self, line: str) -> rs.RunStatus: | ||||
| 	def run(self, line: str, *, macro_only = False) -> rs.RunStatus: | ||||
| 		e = Parser.parse_line(line) | ||||
|  | ||||
| 		# If this line is a macro definition, save the macro. | ||||
| @ -67,14 +70,20 @@ class Runner: | ||||
| 				macro_expr = e.exp | ||||
| 			) | ||||
|  | ||||
| 		elif macro_only: | ||||
| 			raise rs.NotAMacro() | ||||
|  | ||||
| 		# If this line is a command, do the command. | ||||
| 		elif isinstance(e, tokens.command): | ||||
| 			return commands.run(e, self) | ||||
| 			commands.run(e, self) | ||||
| 			return rs.CommandStatus(cmd = e.name) | ||||
|  | ||||
| 		# If this line is a plain expression, reduce it. | ||||
| 		elif isinstance(e, tokens.LambdaToken): | ||||
| 			e.bind_variables() | ||||
| 			return self.reduce_expression(e) | ||||
|  | ||||
| 		# We shouldn't ever get here. | ||||
| 		else: | ||||
| 			raise TypeError(f"I don't know what to do with a {type(e)}") | ||||
|  | ||||
|  | ||||
| @ -4,6 +4,16 @@ import enum | ||||
|  | ||||
| import lamb.tokens as tokens | ||||
|  | ||||
|  | ||||
| class NotAMacro(Exception): | ||||
| 	""" | ||||
| 	Raised when we try to run a non-macro line | ||||
| 	while enforcing macro_only in Runner.run(). | ||||
|  | ||||
| 	This should be caught and elegantly presented to the user. | ||||
| 	""" | ||||
| 	pass | ||||
|  | ||||
| class RunStatus: | ||||
| 	""" | ||||
| 	Base class for run status. | ||||
| @ -64,14 +74,11 @@ class ReduceStatus(RunStatus): | ||||
| class CommandStatus(RunStatus): | ||||
| 	""" | ||||
| 	Returned when a command is executed. | ||||
| 	Doesn't do anything interesting. | ||||
|  | ||||
| 	Values: | ||||
| 	`formatted_text`: What to print after this command is executed | ||||
| 	`cmd`: The command that was run, without a colon. | ||||
| 	""" | ||||
|  | ||||
| 	def __init__( | ||||
| 		self, | ||||
| 		*, | ||||
| 		formatted_text: FormattedText | HTML | ||||
| 	): | ||||
| 		self.formatted_text = formatted_text | ||||
| 	def __init__(self, *, cmd: str): | ||||
| 		self.cmd = cmd | ||||
| @ -36,6 +36,7 @@ style = Style.from_dict({ | ||||
| 	"warn": "#FFFF00", | ||||
| 	"err": "#FF0000", | ||||
| 	"prompt": "#00FFFF", | ||||
| 	"ok": "#B4EC85", | ||||
|  | ||||
| 	# Syntax | ||||
| 	"syn_macro": "#FF00FF", | ||||
|  | ||||
		Reference in New Issue
	
	Block a user