From da430cca565c7828d821f42575ceba2b770de077 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 11 Feb 2026 15:07:41 -0500 Subject: [PATCH 01/35] updated README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46ac9e8..19184b7 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Status -[![.github/workflows/ci.yml](https://github.com/clause/471c/actions/workflows/ci.yml/badge.svg)](https://github.com/clause/471c/actions/workflows/ci.yml) -[![Coverage](https://codecov.io/gh/clause/471c/branch/main/graph/badge.svg)](https://codecov.io/gh/clause/471c) +[![.github/workflows/ci.yml](https://github.com/jtfulky/471c/actions/workflows/ci.yml/badge.svg)](https://github.com/jtfulky/471c/actions/workflows/ci.yml) +[![Coverage](https://codecov.io/gh/jtfulky/471c/branch/main/graph/badge.svg)](https://codecov.io/gh/jtfulky/471c) # Contributing From 4757202f0a6cd6987120a04e2c89bf5699e57a95 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Feb 2026 12:28:41 -0500 Subject: [PATCH 02/35] L3 README with some inclass work on tests/check implementation --- packages/L1/README.md | 11 +++++++++++ packages/L3/src/L3/check.py | 13 ++++++++----- packages/L3/test/L3/test_check.py | 10 ++++++++++ 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/packages/L1/README.md b/packages/L1/README.md index e69de29..919bc4a 100644 --- a/packages/L1/README.md +++ b/packages/L1/README.md @@ -0,0 +1,11 @@ +L3 + +The L3 Compiler is an expression based language with named variables, functions, arithmetic, conditionals, and explicit heap operations. There is an Identifier that names variables and functions as well as a Program that contains parameters and a single term body. A term is a unioned type of all avalible constructs. + +Bindings and variables let and letrec bind different identifiers to terms; and then reference reads a bound name. Unlike let, letrec supports self-reference for recursive definitions. + +For functions, there is an abstract that defines the function with a list of parameters and a body. Apply is used to invoke a function with given arguments. + +For values and control we have immediate, primative, and branch. Immediate is an integer literal and primitive applies the +, -, or * operator to a left and right term. Branch is then able to be used to evaluate < or == and selects consequent or otherwise since the langage does not support boolean values. + +The last thing that L3 supports is memory and sequencing. Allocate reserves a block of memory, while load reads from the memory and store writes to memory. There is also begin which sequences terms before producing a final value. \ No newline at end of file diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index 40fdfe5..8ed4193 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -34,8 +34,10 @@ def check_term( case LetRec(): pass - case Reference(): - pass + case Reference(name=name): + match context.get(name): + case None: + raise NameError(f"Undefined variable: {name}") case Abstract(): pass @@ -43,11 +45,12 @@ def check_term( case Apply(): pass - case Immediate(): + case Immediate(value=_value): pass - case Primitive(): - pass + case Primitive(operator=_operator, left=left, right=right): + recur(left) + recur(right) case Branch(): pass diff --git a/packages/L3/test/L3/test_check.py b/packages/L3/test/L3/test_check.py index 88ff64a..603a7f4 100644 --- a/packages/L3/test/L3/test_check.py +++ b/packages/L3/test/L3/test_check.py @@ -2,6 +2,8 @@ from L3.check import Context, check_term from L3.syntax import Reference +from packages.L0.src.L0.syntax import Immediate + @pytest.mark.skip() def test_check_reference_bound(): @@ -22,3 +24,11 @@ def test_check_reference_free(): with pytest.raises(ValueError): check_term(term, context) + + +def test_check_intermediate(): + term = Immediate(value=0) + + context: Context = {} + + check_term(term, context) From b985268f7033709ffee2b53d9a6627f83ff69f53 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Feb 2026 12:44:51 -0500 Subject: [PATCH 03/35] I put the L3 readme in the wrong file so fixed that and wrote the L2 readme. --- packages/L1/README.md | 11 ----------- packages/L2/README.md | 3 +++ packages/L3/README.md | 11 +++++++++++ 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/packages/L1/README.md b/packages/L1/README.md index 919bc4a..e69de29 100644 --- a/packages/L1/README.md +++ b/packages/L1/README.md @@ -1,11 +0,0 @@ -L3 - -The L3 Compiler is an expression based language with named variables, functions, arithmetic, conditionals, and explicit heap operations. There is an Identifier that names variables and functions as well as a Program that contains parameters and a single term body. A term is a unioned type of all avalible constructs. - -Bindings and variables let and letrec bind different identifiers to terms; and then reference reads a bound name. Unlike let, letrec supports self-reference for recursive definitions. - -For functions, there is an abstract that defines the function with a list of parameters and a body. Apply is used to invoke a function with given arguments. - -For values and control we have immediate, primative, and branch. Immediate is an integer literal and primitive applies the +, -, or * operator to a left and right term. Branch is then able to be used to evaluate < or == and selects consequent or otherwise since the langage does not support boolean values. - -The last thing that L3 supports is memory and sequencing. Allocate reserves a block of memory, while load reads from the memory and store writes to memory. There is also begin which sequences terms before producing a final value. \ No newline at end of file diff --git a/packages/L2/README.md b/packages/L2/README.md index e69de29..5fba267 100644 --- a/packages/L2/README.md +++ b/packages/L2/README.md @@ -0,0 +1,3 @@ +L2 + +L2 has most of the same characteristics as L3 except letrec is missing. Functions, arithmetic, branching, and heap management all remain the same but not having letrec means that recursive bindings are not avalible. \ No newline at end of file diff --git a/packages/L3/README.md b/packages/L3/README.md index e69de29..c22df31 100644 --- a/packages/L3/README.md +++ b/packages/L3/README.md @@ -0,0 +1,11 @@ +L3 + +The L3 Compiler is an expression based language with named variables, functions, arithmetic, conditionals, and explicit heap operations. There is an Identifier that names variables and functions as well as a Program that contains parameters and a single term body. A term is a unioned type of all avalible constructs. + +Bindings and variables let and letrec bind different identifiers to terms; and then reference reads a bound name. Unlike let, letrec supports self-reference for recursive definitions. + +For functions, there is an abstract that defines the function with a list of parameters and a body. Apply is used to invoke a function with given arguments. + +For values and control we have immediate, primative, and branch. Immediate is an integer literal and primitive applies the +, -, or * operator to a left and right term. Branch is then able to be used to evaluate < or == and selects a consequent or otherwise. + +The last thing that L3 supports is memory and sequencing. Allocate reserves a block of memory, while load reads from the memory and store writes to memory. There is also begin which sequences terms before producing a final value. \ No newline at end of file From 126028c4ea785dcc7cd2f4652cc0c10257167cf0 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Feb 2026 21:41:55 -0500 Subject: [PATCH 04/35] finished l1 and l0 readme content --- packages/L0/README.md | 3 +++ packages/L1/README.md | 3 +++ 2 files changed, 6 insertions(+) diff --git a/packages/L0/README.md b/packages/L0/README.md index e69de29..da6e2bc 100644 --- a/packages/L0/README.md +++ b/packages/L0/README.md @@ -0,0 +1,3 @@ +L0 + +L0 is now a procedure based language as compared to our previous expression and statement languages. Here, a program has a sequence of named procedures each containing its own parameters and a statement body. A statement can have copy, immediate, primative, branch, allocate, load, store, address, call, and hault. The main differences are that L0 organizes code into multiple named procedures whereas L1 had a single extry program with a statement body. L1 also uses abstract and apply for its functions where L0 replaces them with address and call removing a layer of abstraction. L0 also makes procedures ex plicity and predefined rather than in others where they were constructred functions at runtime. \ No newline at end of file diff --git a/packages/L1/README.md b/packages/L1/README.md index e69de29..6eaeb81 100644 --- a/packages/L1/README.md +++ b/packages/L1/README.md @@ -0,0 +1,3 @@ +L1 + +Now in L1 it is a statement based language with very explicit flow control. There exists a program which has parameters and a body, and the statements are similar to L2 and L3 with a few differences. We have copy, immediate, primative, branch, abstract, apply, allocate, load, store, and hault. The differences with L1 as compared to L2 and L3 is that L2 used nested terms as expressions while now L1 replaces all of those with sequential statements. Doing this makes flow control explicit as each statement has a then continuation, except for apply and hault. L1 also adds a copy ability to move values between identifiers, and hault to end the program. L1 removes the idea of let, reference, and begin. \ No newline at end of file From 456ce243f95e06771506c0d40c9b7ab0eba50789 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Feb 2026 21:42:35 -0500 Subject: [PATCH 05/35] in class work --- packages/L3/src/L3/check.py | 62 +++++++++++++++++----- packages/L3/test/L3/test_check.py | 85 ++++++++++++++++++++++++++++--- 2 files changed, 128 insertions(+), 19 deletions(-) diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index 8ed4193..403fff7 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -1,5 +1,6 @@ from collections.abc import Mapping from functools import partial +from typing import Counter from .syntax import ( Abstract, @@ -13,6 +14,7 @@ LetRec, Load, Primitive, + Program, Reference, Store, Term, @@ -28,27 +30,50 @@ def check_term( recur = partial(check_term, context=context) # noqa: F841 match term: - case Let(): - pass + case Let(bindings=bindings, body=body): + counts = Counter(name for name, _ in bindings) + duplicates = {name: count for name, count in counts.items() if count > 1} + if duplicates: + raise ValueError(f"Duplicate bindings: {duplicates}") - case LetRec(): - pass + for _, value in bindings: + recur(value) - case Reference(name=name): - match context.get(name): - case None: - raise NameError(f"Undefined variable: {name}") + local = dict.fromkeys([name for name, _ in bindings]) + recur(body, context={**context, **local}) - case Abstract(): - pass + case LetRec(bindings=bindings, body=body): + counts = Counter(name for name, _ in bindings) + duplicates = {name: count for name, count in counts.items() if count > 1} + if duplicates: + raise ValueError(f"Duplicate bindings: {duplicates}") + + local = dict.fromkeys([name for name, _ in bindings]) + + for name, value in bindings: + recur(value, context={**context, **local}) - case Apply(): + check_term(body, context={**context, **local}) + + case Reference(name=name): # Leaf + if name not in context: + raise ValueError(f"Unbound variable: {name}") + + case Abstract(parameters=parameters, body=body): # Done + counts = Counter(parameters) + duplicates = {name for name, count in counts.items() if count > 1} + if duplicates: + raise ValueError(f"Duplicate parameters: {duplicates}") + local = dict.fromkeys(parameters, None) + check_term(body, context=local) + + case Apply(target=target, arguments=arguments): pass - case Immediate(value=_value): + case Immediate(value=_value): # Leaf pass - case Primitive(operator=_operator, left=left, right=right): + case Primitive(operator=_operator, left=left, right=right): # Should be done recur(left) recur(right) @@ -66,3 +91,14 @@ def check_term( case Begin(): # pragma: no branch pass + + +def check_program(program: Program) -> None: + match program: + case Program(parameters=parameters, body=body): # pragma: no branch + counts = Counter(parameters) + duplicates = {name for name, count in counts.items() if count > 1} + if duplicates: + raise ValueError(f"Duplicate parameters: {duplicates}") + local = dict.fromkeys(parameters, None) + check_term(body, context=local) diff --git a/packages/L3/test/L3/test_check.py b/packages/L3/test/L3/test_check.py index 603a7f4..090b737 100644 --- a/packages/L3/test/L3/test_check.py +++ b/packages/L3/test/L3/test_check.py @@ -1,11 +1,8 @@ import pytest -from L3.check import Context, check_term -from L3.syntax import Reference +from L3.check import Context, check_program, check_term +from L3.syntax import Immediate, Let, LetRec, Program, Reference -from packages.L0.src.L0.syntax import Immediate - -@pytest.mark.skip() def test_check_reference_bound(): term = Reference(name="x") @@ -16,7 +13,6 @@ def test_check_reference_bound(): check_term(term, context) -@pytest.mark.skip() def test_check_reference_free(): term = Reference(name="x") @@ -32,3 +28,80 @@ def test_check_intermediate(): context: Context = {} check_term(term, context) + + +def test_check_term_let(): + term = Let( + bindings=[ + ("x", Immediate(value=0)), + ], + body=Reference(name="x"), + ) + + context: Context = {} + + check_term(term, context) + + +def test_check_term_let_duplicate(): + term = Let( + bindings=[ + ("x", Immediate(value=0)), + ("x", Immediate(value=1)), + ], + body=Reference(name="x"), + ) + + context: Context = {} + + with pytest.raises(ValueError): + check_term(term, context) + + +def test_check_term_not_avaliable_in_initializer(): + term = Let( + bindings=[ + ("x", Immediate(value=0)), + ("y", Reference(name="x")), + ], + body=Reference(name="x"), + ) + + context: Context = {} + + with pytest.raises(ValueError): + check_term(term, context) + + +def test_check_term_letrec_avalible_in_initializers(): + term = LetRec( + bindings=[ + ("x", Immediate(value=0)), + ("y", Reference(name="x")), + ], + body=Reference(name="x"), + ) + + context: Context = {} + + with pytest.raises(ValueError): + check_term(term, context) + + +def test_check_program_duplicate(): + program = Program( + parameters=["x", "x"], + body=Immediate(value=0), + ) + + with pytest.raises(ValueError): + check_program(program) + + +def test_check_program(): + program = Program( + parameters=["x"], + body=Reference(name="x"), + ) + + check_program(program) From dfc2ffe3143a2abe6fe6da51ccab431f92eb4c8c Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 18 Feb 2026 15:06:52 -0500 Subject: [PATCH 06/35] Added class example but need to figure out how to get my stash. Pulled fork upstream and overwrote some changes. --- packages/L3/src/L3/check.py | 11 ++-- packages/L3/src/L3/eliminate_letrec.txt | 70 +++++++++++++++++++++++++ packages/L3/test/L3/test_check.py | 20 ------- 3 files changed, 75 insertions(+), 26 deletions(-) create mode 100644 packages/L3/src/L3/eliminate_letrec.txt diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index 6b5d965..a960c9d 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -1,7 +1,6 @@ from collections import Counter from collections.abc import Mapping from functools import partial -from typing import Counter from .syntax import ( Abstract, @@ -35,7 +34,7 @@ def check_term( counts = Counter(name for name, _ in bindings) duplicates = {name: count for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"Duplicate bindings: {duplicates}") + raise ValueError(f"Duplicate Bindings: {duplicates}") for _, value in bindings: recur(value) @@ -47,7 +46,7 @@ def check_term( counts = Counter(name for name, _ in bindings) duplicates = {name: count for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"duplicate binders: {duplicates}") + raise ValueError(f"Duplicate Binders: {duplicates}") local = dict.fromkeys([name for name, _ in bindings]) @@ -58,13 +57,13 @@ def check_term( case Reference(name=name): if name not in context: - raise ValueError(f"unknown variable: {name}") + raise ValueError(f"Unknown Variable: {name}") case Abstract(parameters=parameters, body=body): counts = Counter(parameters) duplicates = {name for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"duplicate parameters: {duplicates}") + raise ValueError(f"Duplicate Parameters: {duplicates}") local = dict.fromkeys(parameters, None) recur(body, context={**context, **local}) @@ -111,7 +110,7 @@ def check_program( counts = Counter(parameters) duplicates = {name for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"duplicate parameters: {duplicates}") + raise ValueError(f"Duplicate Parameters: {duplicates}") local = dict.fromkeys(parameters, None) check_term(body, context=local) diff --git a/packages/L3/src/L3/eliminate_letrec.txt b/packages/L3/src/L3/eliminate_letrec.txt new file mode 100644 index 0000000..d6ae831 --- /dev/null +++ b/packages/L3/src/L3/eliminate_letrec.txt @@ -0,0 +1,70 @@ +from collections import Counter +from functools import partial +from typing import Counter + +from L2 import syntax as L2 + +from . import syntax as L3 + +type Context = None + + +def eliminate_letrec_term( + term: L3.Term, + context: Context, +) -> L2.Term: + recur = partial(eliminate_letrec_term, context=context) + + match term: + case Let(bindings=bindings, body=body): + pass + + local = dict.fromkeys([name for name, _ in bindings]) + recur(body, context={**context, **local}) + + case LetRec(bindings=bindings, body=body): + pass + + case Reference(name=name): + pass + + case Abstract(parameters=parameters, body=body): + pass + + case Apply(target=target, arguments=arguments): + pass + + case Immediate(value=_value): + pass + + case Primitive(operator=_operator, left=left, right=right): + pass + + case Branch(operator=_operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + pass + + case Allocate(count=_count): + pass + + case Load(base=base, index=_index): + pass + + case Store(base=base, index=_index, value=value): + pass + + case Begin(effects=effects, value=value): # pragma: no branch + pass + + +def check_program( + program: L3.Program, +) -> L2.Program: + match program: + case Program(parameters=parameters, body=body): # pragma: no branch + counts = Counter(parameters) + duplicates = {name for name, count in counts.items() if count > 1} + if duplicates: + raise ValueError(f"Duplicate Parameters: {duplicates}") + + local = dict.fromkeys(parameters, None) + check_term(body, context=local) diff --git a/packages/L3/test/L3/test_check.py b/packages/L3/test/L3/test_check.py index 9b1e51a..885d886 100644 --- a/packages/L3/test/L3/test_check.py +++ b/packages/L3/test/L3/test_check.py @@ -17,7 +17,6 @@ ) -@pytest.mark.skip def test_check_term_let(): term = Let( bindings=[ @@ -31,7 +30,6 @@ def test_check_term_let(): check_term(term, context) -@pytest.mark.skip def test_check_term_let_scope(): term = Let( bindings=[ @@ -47,7 +45,6 @@ def test_check_term_let_scope(): check_term(term, context) -@pytest.mark.skip def test_check_term_let_duplicate_binders(): term = Let( bindings=[ @@ -63,7 +60,6 @@ def test_check_term_let_duplicate_binders(): check_term(term, context) -@pytest.mark.skip def test_check_term_letrec(): term = LetRec( bindings=[ @@ -77,7 +73,6 @@ def test_check_term_letrec(): check_term(term, context) -@pytest.mark.skip def test_check_term_letrec_scope(): term = LetRec( bindings=[ @@ -92,7 +87,6 @@ def test_check_term_letrec_scope(): check_term(term, context) -@pytest.mark.skip def test_check_term_letrec_duplicate_binders(): term = LetRec( bindings=[ @@ -108,7 +102,6 @@ def test_check_term_letrec_duplicate_binders(): check_term(term, context) -@pytest.mark.skip def test_check_term_reference_bound(): term = Reference(name="x") @@ -119,7 +112,6 @@ def test_check_term_reference_bound(): check_term(term, context) -@pytest.mark.skip def test_check_term_reference_free(): term = Reference(name="x") @@ -129,7 +121,6 @@ def test_check_term_reference_free(): check_term(term, context) -@pytest.mark.skip def test_check_term_abstract(): term = Abstract( parameters=["x"], @@ -141,7 +132,6 @@ def test_check_term_abstract(): check_term(term, context) -@pytest.mark.skip def test_check_term_abstract_duplicate_parameters(): term = Abstract( parameters=["x", "x"], @@ -154,7 +144,6 @@ def test_check_term_abstract_duplicate_parameters(): check_term(term, context) -@pytest.mark.skip def test_check_term_apply(): term = Apply( target=Reference(name="x"), @@ -168,7 +157,6 @@ def test_check_term_apply(): check_term(term, context) -@pytest.mark.skip def test_check_term_immediate(): term = Immediate(value=0) @@ -177,7 +165,6 @@ def test_check_term_immediate(): check_term(term, context) -@pytest.mark.skip def test_check_term_primitive(): term = Primitive( operator="+", @@ -190,7 +177,6 @@ def test_check_term_primitive(): check_term(term, context) -@pytest.mark.skip def test_check_term_branch(): term = Branch( operator="<", @@ -205,7 +191,6 @@ def test_check_term_branch(): check_term(term, context) -@pytest.mark.skip def test_check_term_allocate(): term = Allocate(count=0) @@ -214,7 +199,6 @@ def test_check_term_allocate(): check_term(term, context) -@pytest.mark.skip def test_check_term_load(): term = Load( base=Reference(name="x"), @@ -228,7 +212,6 @@ def test_check_term_load(): check_term(term, context) -@pytest.mark.skip def test_check_term_store(): term = Store( base=Reference(name="x"), @@ -243,7 +226,6 @@ def test_check_term_store(): check_term(term, context) -@pytest.mark.skip def test_check_term_begin(): term = Begin( effects=[Immediate(value=0)], @@ -255,7 +237,6 @@ def test_check_term_begin(): check_term(term, context) -@pytest.mark.skip def test_check_program(): program = Program( parameters=[], @@ -265,7 +246,6 @@ def test_check_program(): check_program(program) -@pytest.mark.skip def test_check_program_duplicate_parameters(): program = Program( parameters=["x", "x"], From 331be98802c0d815d94846739a617e2a81d5ec63 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 18 Feb 2026 15:09:00 -0500 Subject: [PATCH 07/35] Readme changes for testing --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 19184b7..b93906a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Status -[![.github/workflows/ci.yml](https://github.com/jtfulky/471c/actions/workflows/ci.yml/badge.svg)](https://github.com/jtfulky/471c/actions/workflows/ci.yml) -[![Coverage](https://codecov.io/gh/jtfulky/471c/branch/main/graph/badge.svg)](https://codecov.io/gh/jtfulky/471c) +[![.github/workflows/ci.yml](https://github.com/JTFulkerson/471c/actions/workflows/ci.yml/badge.svg)](https://github.com/JTFulkerson/471c/actions/workflows/ci.yml) +[![Coverage](https://codecov.io/gh/JTFulkerson/471c/branch/main/graph/badge.svg)](https://codecov.io/gh/JTFulkerson/471c) # Contributing From 0d90c17d6c88967a8ce527e21655b3825f37d2b8 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 18 Feb 2026 21:15:55 -0500 Subject: [PATCH 08/35] Restored some of my old changes. --- packages/L3/src/L3/check.py | 60 +++++++++++++++---------------------- 1 file changed, 24 insertions(+), 36 deletions(-) diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index a960c9d..403fff7 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -1,6 +1,6 @@ -from collections import Counter from collections.abc import Mapping from functools import partial +from typing import Counter from .syntax import ( Abstract, @@ -27,14 +27,14 @@ def check_term( term: Term, context: Context, ) -> None: - recur = partial(check_term, context=context) + recur = partial(check_term, context=context) # noqa: F841 match term: case Let(bindings=bindings, body=body): counts = Counter(name for name, _ in bindings) duplicates = {name: count for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"Duplicate Bindings: {duplicates}") + raise ValueError(f"Duplicate bindings: {duplicates}") for _, value in bindings: recur(value) @@ -46,71 +46,59 @@ def check_term( counts = Counter(name for name, _ in bindings) duplicates = {name: count for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"Duplicate Binders: {duplicates}") + raise ValueError(f"Duplicate bindings: {duplicates}") local = dict.fromkeys([name for name, _ in bindings]) for name, value in bindings: recur(value, context={**context, **local}) - check_term(body, {**context, **local}) + check_term(body, context={**context, **local}) - case Reference(name=name): + case Reference(name=name): # Leaf if name not in context: - raise ValueError(f"Unknown Variable: {name}") + raise ValueError(f"Unbound variable: {name}") - case Abstract(parameters=parameters, body=body): + case Abstract(parameters=parameters, body=body): # Done counts = Counter(parameters) duplicates = {name for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"Duplicate Parameters: {duplicates}") - + raise ValueError(f"Duplicate parameters: {duplicates}") local = dict.fromkeys(parameters, None) - recur(body, context={**context, **local}) + check_term(body, context=local) case Apply(target=target, arguments=arguments): - recur(target) - for argument in arguments: - recur(argument) + pass - case Immediate(value=_value): + case Immediate(value=_value): # Leaf pass - case Primitive(operator=_operator, left=left, right=right): + case Primitive(operator=_operator, left=left, right=right): # Should be done recur(left) recur(right) - case Branch(operator=_operator, left=left, right=right, consequent=consequent, otherwise=otherwise): - recur(left) - recur(right) - recur(consequent) - recur(otherwise) + case Branch(): + pass - case Allocate(count=_count): + case Allocate(): pass - case Load(base=base, index=_index): - recur(base) + case Load(): + pass - case Store(base=base, index=_index, value=value): - recur(base) - recur(value) + case Store(): + pass - case Begin(effects=effects, value=value): # pragma: no branch - for effect in effects: - recur(effect) - recur(value) + case Begin(): # pragma: no branch + pass -def check_program( - program: Program, -) -> None: +def check_program(program: Program) -> None: match program: case Program(parameters=parameters, body=body): # pragma: no branch counts = Counter(parameters) duplicates = {name for name, count in counts.items() if count > 1} if duplicates: - raise ValueError(f"Duplicate Parameters: {duplicates}") - + raise ValueError(f"Duplicate parameters: {duplicates}") local = dict.fromkeys(parameters, None) check_term(body, context=local) From f917ade4da18e3447cafd173315c3111b28d0fc8 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 18 Feb 2026 21:21:17 -0500 Subject: [PATCH 09/35] Restored previous stashed changes --- packages/L3/src/L3/check.py | 45 ++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index 403fff7..0314360 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -67,8 +67,10 @@ def check_term( local = dict.fromkeys(parameters, None) check_term(body, context=local) - case Apply(target=target, arguments=arguments): - pass + case Apply(target=target, arguments=arguments): # Done + recur(target) + for argument in arguments: + recur(argument) case Immediate(value=_value): # Leaf pass @@ -77,20 +79,31 @@ def check_term( recur(left) recur(right) - case Branch(): - pass - - case Allocate(): - pass - - case Load(): - pass - - case Store(): - pass - - case Begin(): # pragma: no branch - pass + case Branch(operator=_operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + recur(left) + recur(right) + recur(consequent) + recur(otherwise) + + case Allocate(count=count): + if count < 0: + raise ValueError("Negative allocation count") + + case Load(base=base, index=index): + recur(base) + if index < 0: + raise ValueError("Negative load index") + + case Store(base=base, index=index, value=value): + recur(base) + if index < 0: + raise ValueError("Negative store index") + recur(value) + + case Begin(effects=effects, value=value): # pragma: no branch + for effect in effects: + recur(effect) + recur(value) def check_program(program: Program) -> None: From e5b9a180104e0cfffa6cdbaa466e5600dc4beb0b Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 18 Feb 2026 21:26:28 -0500 Subject: [PATCH 10/35] Removed unnessesary checks on negative index and count values. Non negative integers defined in syntax. --- packages/L3/src/L3/check.py | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/L3/src/L3/check.py b/packages/L3/src/L3/check.py index 0314360..e923785 100644 --- a/packages/L3/src/L3/check.py +++ b/packages/L3/src/L3/check.py @@ -85,19 +85,14 @@ def check_term( recur(consequent) recur(otherwise) - case Allocate(count=count): - if count < 0: - raise ValueError("Negative allocation count") - - case Load(base=base, index=index): - recur(base) - if index < 0: - raise ValueError("Negative load index") - - case Store(base=base, index=index, value=value): - recur(base) - if index < 0: - raise ValueError("Negative store index") + case Allocate(count=_count): + pass # No need to check count, as it is a non-negative integer by construction + + case Load(base=base, index=_index): + recur(base) # No need to check index, as it is a non-negative integer by construction + + case Store(base=base, index=_index, value=value): + recur(base) # No need to check index, as it is a non-negative integer by construction recur(value) case Begin(effects=effects, value=value): # pragma: no branch From 54016bae1d01686ee05dc33a4d6750ca5e179c50 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 20 Feb 2026 14:39:13 -0500 Subject: [PATCH 11/35] Changed to use new pulled elimate letrec, wrote but have not tested let and reference --- packages/L3/src/L3/eliminate_letrec.py | 10 +++- packages/L3/src/L3/eliminate_letrec.txt | 70 ------------------------- 2 files changed, 8 insertions(+), 72 deletions(-) delete mode 100644 packages/L3/src/L3/eliminate_letrec.txt diff --git a/packages/L3/src/L3/eliminate_letrec.py b/packages/L3/src/L3/eliminate_letrec.py index 63ea854..34e5869 100644 --- a/packages/L3/src/L3/eliminate_letrec.py +++ b/packages/L3/src/L3/eliminate_letrec.py @@ -17,7 +17,10 @@ def eliminate_letrec_term( match term: case L3.Let(bindings=bindings, body=body): - pass + return L2.Let( + bindings=[(name, recur(value)) for name, value in bindings], + body=recur(body, context={**context, **dict.fromkeys([name for name, _ in bindings])}), + ) case L3.LetRec(bindings=bindings, body=body): pass @@ -25,7 +28,10 @@ def eliminate_letrec_term( case L3.Reference(name=name): # if name is a recursive variable -> (Load (Reference name))) # else (Reference name) - pass + if name in context: + return L2.Load(base=L2.Reference(name=name), index=0) + else: + return L2.Reference(name=name) case L3.Abstract(parameters=parameters, body=body): pass diff --git a/packages/L3/src/L3/eliminate_letrec.txt b/packages/L3/src/L3/eliminate_letrec.txt deleted file mode 100644 index d6ae831..0000000 --- a/packages/L3/src/L3/eliminate_letrec.txt +++ /dev/null @@ -1,70 +0,0 @@ -from collections import Counter -from functools import partial -from typing import Counter - -from L2 import syntax as L2 - -from . import syntax as L3 - -type Context = None - - -def eliminate_letrec_term( - term: L3.Term, - context: Context, -) -> L2.Term: - recur = partial(eliminate_letrec_term, context=context) - - match term: - case Let(bindings=bindings, body=body): - pass - - local = dict.fromkeys([name for name, _ in bindings]) - recur(body, context={**context, **local}) - - case LetRec(bindings=bindings, body=body): - pass - - case Reference(name=name): - pass - - case Abstract(parameters=parameters, body=body): - pass - - case Apply(target=target, arguments=arguments): - pass - - case Immediate(value=_value): - pass - - case Primitive(operator=_operator, left=left, right=right): - pass - - case Branch(operator=_operator, left=left, right=right, consequent=consequent, otherwise=otherwise): - pass - - case Allocate(count=_count): - pass - - case Load(base=base, index=_index): - pass - - case Store(base=base, index=_index, value=value): - pass - - case Begin(effects=effects, value=value): # pragma: no branch - pass - - -def check_program( - program: L3.Program, -) -> L2.Program: - match program: - case Program(parameters=parameters, body=body): # pragma: no branch - counts = Counter(parameters) - duplicates = {name for name, count in counts.items() if count > 1} - if duplicates: - raise ValueError(f"Duplicate Parameters: {duplicates}") - - local = dict.fromkeys(parameters, None) - check_term(body, context=local) From 11d723fef3fc2f1982aafccbc06bfe9af53477d2 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Thu, 26 Feb 2026 11:33:59 -0500 Subject: [PATCH 12/35] Finished eliminating letrec, not passing all branch tests --- packages/L3/src/L3/eliminate_letrec.py | 35 +++++++++++++++----- packages/L3/test/L3/test_eliminate_letrec.py | 3 -- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/packages/L3/src/L3/eliminate_letrec.py b/packages/L3/src/L3/eliminate_letrec.py index 34e5869..4f668cf 100644 --- a/packages/L3/src/L3/eliminate_letrec.py +++ b/packages/L3/src/L3/eliminate_letrec.py @@ -19,11 +19,14 @@ def eliminate_letrec_term( case L3.Let(bindings=bindings, body=body): return L2.Let( bindings=[(name, recur(value)) for name, value in bindings], - body=recur(body, context={**context, **dict.fromkeys([name for name, _ in bindings])}), + body=recur(body), ) case L3.LetRec(bindings=bindings, body=body): - pass + return L2.Let( + bindings=[(name, recur(value)) for name, value in bindings], + body=recur(body, context={**context, **dict.fromkeys([name for name, _ in bindings])}), + ) case L3.Reference(name=name): # if name is a recursive variable -> (Load (Reference name))) @@ -34,16 +37,23 @@ def eliminate_letrec_term( return L2.Reference(name=name) case L3.Abstract(parameters=parameters, body=body): - pass + return L2.Abstract(parameters=parameters, body=recur(body)) case L3.Apply(target=target, arguments=arguments): - pass + return L2.Apply( + target=recur(target), + arguments=[recur(argument) for argument in arguments], + ) case L3.Immediate(value=value): return L2.Immediate(value=value) - case L3.Primitive(operator=_operator, left=left, right=right): - pass + case L3.Primitive(operator=operator, left=left, right=right): + return L2.Primitive( + operator=operator, + left=recur(left), + right=recur(right), + ) case L3.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): return L2.Branch( @@ -63,11 +73,18 @@ def eliminate_letrec_term( index=index, ) - case L3.Store(base=base, index=_index, value=value): - pass + case L3.Store(base=base, index=index, value=value): + return L2.Store( + base=recur(base), + index=index, + value=recur(value), + ) case L3.Begin(effects=effects, value=value): # pragma: no branch - pass + return L2.Begin( + effects=[recur(effect) for effect in effects], + value=recur(value), + ) def eliminate_letrec_program( diff --git a/packages/L3/test/L3/test_eliminate_letrec.py b/packages/L3/test/L3/test_eliminate_letrec.py index 299a79c..3438c6a 100644 --- a/packages/L3/test/L3/test_eliminate_letrec.py +++ b/packages/L3/test/L3/test_eliminate_letrec.py @@ -1,10 +1,8 @@ -import pytest from L2 import syntax as L2 from L3 import syntax as L3 from L3.eliminate_letrec import Context, eliminate_letrec_program, eliminate_letrec_term -@pytest.mark.skip def test_check_term_let(): term = L3.Let( bindings=[ @@ -27,7 +25,6 @@ def test_check_term_let(): assert actual == expected -@pytest.mark.skip def test_eliminate_letrec_program(): program = L3.Program( parameters=[], From e83aa3e6fcd59ad8ea3681761f69de86bb0d77ae Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 27 Feb 2026 20:38:22 -0500 Subject: [PATCH 13/35] Fixed tests to have 100% branch coverage --- packages/L3/test/L3/test_eliminate_letrec.py | 128 +++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/packages/L3/test/L3/test_eliminate_letrec.py b/packages/L3/test/L3/test_eliminate_letrec.py index 3438c6a..2ea810e 100644 --- a/packages/L3/test/L3/test_eliminate_letrec.py +++ b/packages/L3/test/L3/test_eliminate_letrec.py @@ -39,3 +39,131 @@ def test_eliminate_letrec_program(): actual = eliminate_letrec_program(program) assert actual == expected + + +def test_eliminate_letrec_reference_recursive(): + term = L3.Reference(name="x") + + context: Context = {"x": None} + + expected = L2.Load( + base=L2.Reference(name="x"), + index=0, + ) + + actual = eliminate_letrec_term(term, context) + + assert actual == expected + + +def test_eliminate_letrec_reference_non_recursive(): + term = L3.Reference(name="x") + + context: Context = {} + + expected = L2.Reference(name="x") + + actual = eliminate_letrec_term(term, context) + + assert actual == expected + + +def test_eliminate_letrec_binding_self_reference(): + term = L3.LetRec( + bindings=[("x", L3.Reference(name="x"))], + body=L3.Immediate(value=0), + ) + + expected = L2.Let( + bindings=[ + ( + "x", + L2.Reference(name="x"), + ) + ], + body=L2.Immediate(value=0), + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected + + +def test_eliminate_letrec_abstract_apply(): + term = L3.Apply( + target=L3.Abstract(parameters=["x"], body=L3.Reference(name="x")), + arguments=[L3.Immediate(value=1)], + ) + + expected = L2.Apply( + target=L2.Abstract(parameters=["x"], body=L2.Reference(name="x")), + arguments=[L2.Immediate(value=1)], + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected + + +def test_eliminate_letrec_primitive_branch(): + term = L3.Branch( + operator="<", + left=L3.Primitive( + operator="+", + left=L3.Immediate(value=1), + right=L3.Immediate(value=2), + ), + right=L3.Immediate(value=4), + consequent=L3.Immediate(value=5), + otherwise=L3.Immediate(value=6), + ) + + expected = L2.Branch( + operator="<", + left=L2.Primitive( + operator="+", + left=L2.Immediate(value=1), + right=L2.Immediate(value=2), + ), + right=L2.Immediate(value=4), + consequent=L2.Immediate(value=5), + otherwise=L2.Immediate(value=6), + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected + + +def test_eliminate_letrec_memory_and_begin(): + term = L3.Begin( + effects=[ + L3.Store( + base=L3.Reference(name="b"), + index=0, + value=L3.Immediate(value=2), + ), + ], + value=L3.Load( + base=L3.Allocate(count=1), + index=0, + ), + ) + + expected = L2.Begin( + effects=[ + L2.Store( + base=L2.Reference(name="b"), + index=0, + value=L2.Immediate(value=2), + ) + ], + value=L2.Load( + base=L2.Allocate(count=1), + index=0, + ), + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected From c6f5b309d069ef1aa8ac3dd3e1405ed6baf73b99 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 27 Feb 2026 20:53:03 -0500 Subject: [PATCH 14/35] Skipping parse tests --- packages/L3/test/L3/test_parse.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/L3/test/L3/test_parse.py b/packages/L3/test/L3/test_parse.py index f48391a..2b06f14 100644 --- a/packages/L3/test/L3/test_parse.py +++ b/packages/L3/test/L3/test_parse.py @@ -1,3 +1,4 @@ +import pytest from L3.parse import parse_program, parse_term from L3.syntax import ( Abstract, @@ -17,6 +18,7 @@ # Let +@pytest.mark.skip() def test_parse_let_empty(): source = "(let () x)" @@ -30,6 +32,7 @@ def test_parse_let_empty(): assert actual == expected +@pytest.mark.skip() def test_parse_let_bindings(): source = "(let ((x 0)) x)" @@ -46,6 +49,7 @@ def test_parse_let_bindings(): # LetRec +@pytest.mark.skip() def test_parse_letrec_empty(): source = "(letrec () x)" @@ -59,6 +63,7 @@ def test_parse_letrec_empty(): assert actual == expected +@pytest.mark.skip() def test_parse_letrec_bindings(): source = "(letrec ((x 0)) x)" @@ -75,6 +80,7 @@ def test_parse_letrec_bindings(): # Reference +@pytest.mark.skip() def test_parse_reference(): source = "x" @@ -88,6 +94,7 @@ def test_parse_reference(): # Abstract +@pytest.mark.skip() def test_parse_abstract(): source = "(\\ (x) x)" @@ -102,6 +109,7 @@ def test_parse_abstract(): # Apply +@pytest.mark.skip() def test_parse_apply_empty(): source = "(x)" @@ -115,6 +123,7 @@ def test_parse_apply_empty(): assert actual == expected +@pytest.mark.skip() def test_parse_apply_arguments(): source = "(x y z)" @@ -129,6 +138,7 @@ def test_parse_apply_arguments(): # Immediate +@pytest.mark.skip() def test_parse_immediate(): source = "42" @@ -140,6 +150,7 @@ def test_parse_immediate(): # Primitive +@pytest.mark.skip() def test_parse_add(): source = "(+ 1 2)" @@ -154,6 +165,7 @@ def test_parse_add(): assert actual == expected +@pytest.mark.skip() def test_parse_subtract(): source = "(- 3 2)" @@ -168,6 +180,7 @@ def test_parse_subtract(): assert actual == expected +@pytest.mark.skip() def test_parse_multiply(): source = "(* 2 3)" expected = Primitive( @@ -180,6 +193,7 @@ def test_parse_multiply(): # Branch +@pytest.mark.skip() def test_parse_less_than(): source = "(if (< 1 2) 1 0)" @@ -196,6 +210,7 @@ def test_parse_less_than(): assert actual == expected +@pytest.mark.skip() def test_parse_equal_to(): source = "(if (== 1 1) 1 0)" @@ -213,6 +228,7 @@ def test_parse_equal_to(): # Allocate +@pytest.mark.skip() def test_parse_allocate(): source = "(allocate 0)" @@ -226,6 +242,7 @@ def test_parse_allocate(): # Load +@pytest.mark.skip() def test_parse_load(): source = "(load x 0)" @@ -240,6 +257,7 @@ def test_parse_load(): # Store +@pytest.mark.skip() def test_parse_store(): source = "(store x 0 1)" @@ -254,6 +272,7 @@ def test_parse_store(): assert actual == expected +@pytest.mark.skip() def test_parse_begin(): source = "(begin x)" @@ -267,6 +286,7 @@ def test_parse_begin(): assert actual == expected +@pytest.mark.skip() def test_parse_begin_effects(): source = "(begin x y z)" @@ -284,6 +304,7 @@ def test_parse_begin_effects(): # Program +@pytest.mark.skip() def test_parse_program_identity(): source = "(l3 (x) x)" From 85c43d7428463d371cda9ec6bd7795cf21a1c569 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 27 Feb 2026 21:03:32 -0500 Subject: [PATCH 15/35] added some test, should be at 100% code covereage now --- packages/L3/test/L3/test_eliminate_letrec.py | 112 +++++++++++++++---- 1 file changed, 92 insertions(+), 20 deletions(-) diff --git a/packages/L3/test/L3/test_eliminate_letrec.py b/packages/L3/test/L3/test_eliminate_letrec.py index 2ea810e..137ec1c 100644 --- a/packages/L3/test/L3/test_eliminate_letrec.py +++ b/packages/L3/test/L3/test_eliminate_letrec.py @@ -42,17 +42,15 @@ def test_eliminate_letrec_program(): def test_eliminate_letrec_reference_recursive(): - term = L3.Reference(name="x") - - context: Context = {"x": None} - - expected = L2.Load( - base=L2.Reference(name="x"), - index=0, + term = L3.LetRec( + bindings=[("x", L3.Immediate(value=0))], + body=L3.Reference(name="x"), ) - - actual = eliminate_letrec_term(term, context) - + expected = L2.Let( + bindings=[("x", L2.Immediate(value=0))], + body=L2.Load(base=L2.Reference(name="x"), index=0), + ) + actual = eliminate_letrec_term(term, context={}) assert actual == expected @@ -68,20 +66,15 @@ def test_eliminate_letrec_reference_non_recursive(): assert actual == expected -def test_eliminate_letrec_binding_self_reference(): +def test_eliminate_letrec_body_uses_recursive_binding(): term = L3.LetRec( - bindings=[("x", L3.Reference(name="x"))], - body=L3.Immediate(value=0), + bindings=[("x", L3.Immediate(value=1))], + body=L3.Reference(name="x"), ) expected = L2.Let( - bindings=[ - ( - "x", - L2.Reference(name="x"), - ) - ], - body=L2.Immediate(value=0), + bindings=[("x", L2.Immediate(value=1))], + body=L2.Load(base=L2.Reference(name="x"), index=0), ) actual = eliminate_letrec_term(term, context={}) @@ -135,6 +128,51 @@ def test_eliminate_letrec_primitive_branch(): assert actual == expected +def test_eliminate_letrec_nested_primitives(): + term = L3.Primitive( + operator="+", + left=L3.Primitive( + operator="+", + left=L3.Immediate(value=1), + right=L3.Immediate(value=2), + ), + right=L3.Immediate(value=3), + ) + expected = L2.Primitive( + operator="+", + left=L2.Primitive( + operator="+", + left=L2.Immediate(value=1), + right=L2.Immediate(value=2), + ), + right=L2.Immediate(value=3), + ) + actual = eliminate_letrec_term(term, context={}) + assert actual == expected + + +def test_eliminate_letrec_branch_equals(): + term = L3.Branch( + operator="==", + left=L3.Immediate(value=1), + right=L3.Immediate(value=1), + consequent=L3.Immediate(value=7), + otherwise=L3.Immediate(value=8), + ) + + expected = L2.Branch( + operator="==", + left=L2.Immediate(value=1), + right=L2.Immediate(value=1), + consequent=L2.Immediate(value=7), + otherwise=L2.Immediate(value=8), + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected + + def test_eliminate_letrec_memory_and_begin(): term = L3.Begin( effects=[ @@ -167,3 +205,37 @@ def test_eliminate_letrec_memory_and_begin(): actual = eliminate_letrec_term(term, context={}) assert actual == expected + + +def test_eliminate_letrec_allocate_and_load(): + term = L3.Load( + base=L3.Allocate(count=1), + index=0, + ) + + expected = L2.Load( + base=L2.Allocate(count=1), + index=0, + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected + + +def test_eliminate_letrec_store(): + term = L3.Store( + base=L3.Allocate(count=1), + index=0, + value=L3.Immediate(value=42), + ) + + expected = L2.Store( + base=L2.Allocate(count=1), + index=0, + value=L2.Immediate(value=42), + ) + + actual = eliminate_letrec_term(term, context={}) + + assert actual == expected From 213df948c575a3abdbed47d556cea47726695b25 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Thu, 5 Mar 2026 15:39:58 -0500 Subject: [PATCH 16/35] I think I finished the lark logic, need to finish parse and then can test. --- packages/L3/src/L3/L3.lark | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/packages/L3/src/L3/L3.lark b/packages/L3/src/L3/L3.lark index a7f21d7..f2e0dc0 100644 --- a/packages/L3/src/L3/L3.lark +++ b/packages/L3/src/L3/L3.lark @@ -1,3 +1,6 @@ +%import common.WS +%ignore WS + program : "(" PROGRAM "(" parameters ")" term ")" parameters : IDENTIFIER* @@ -20,11 +23,40 @@ let : "(" LET "(" bindings ")" term ")" letrec : "(" LETREC "(" bindings ")" term ")" bindings : binding* -binding : IDENTIFIER term +binding : "(" IDENTIFIER term ")" + +reference : IDENTIFIER + +abstract : "(" LAMBDA "(" parameters ")" term ")" + +apply : "(" term term* ")" + +immediate : NUMBER + +primitive : "(" OPERATOR term term ")" + +branch : "(" IF "(" COMPARATOR term term ")" term term ")" + +allocate : "(" ALLOCATE immediate ")" + +load : "(" LOAD term immediate ")" + +store : "(" STORE term immediate term ")" + +begin : "(" BEGIN term+ ")" PROGRAM.2 : "l3" LET.2 : "let" LETREC.2 : "letrec" LAMBDA.2 : "\\" | "lambda" | "λ" +IF.2 : "if" +ALLOCATE.2 : "allocate" +LOAD.2 : "load" +STORE.2 : "store" +BEGIN.2 : "begin" + +OPERATOR : "+" | "-" | "*" +COMPARATOR : "<" | "==" -IDENTIFIER : /[a-zA-Z_][a-zA-Z0-9_]*/ \ No newline at end of file +IDENTIFIER : /[a-zA-Z_][a-zA-Z0-9_]*/ +NUMBER : /-?\d+/ \ No newline at end of file From c49323fb3e304ccd5f1cf4816a22a2e6bc1a3dbe Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Thu, 5 Mar 2026 15:43:43 -0500 Subject: [PATCH 17/35] Removed the skip from all tests --- packages/L3/test/L3/test_parse.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/L3/test/L3/test_parse.py b/packages/L3/test/L3/test_parse.py index 2b06f14..f48391a 100644 --- a/packages/L3/test/L3/test_parse.py +++ b/packages/L3/test/L3/test_parse.py @@ -1,4 +1,3 @@ -import pytest from L3.parse import parse_program, parse_term from L3.syntax import ( Abstract, @@ -18,7 +17,6 @@ # Let -@pytest.mark.skip() def test_parse_let_empty(): source = "(let () x)" @@ -32,7 +30,6 @@ def test_parse_let_empty(): assert actual == expected -@pytest.mark.skip() def test_parse_let_bindings(): source = "(let ((x 0)) x)" @@ -49,7 +46,6 @@ def test_parse_let_bindings(): # LetRec -@pytest.mark.skip() def test_parse_letrec_empty(): source = "(letrec () x)" @@ -63,7 +59,6 @@ def test_parse_letrec_empty(): assert actual == expected -@pytest.mark.skip() def test_parse_letrec_bindings(): source = "(letrec ((x 0)) x)" @@ -80,7 +75,6 @@ def test_parse_letrec_bindings(): # Reference -@pytest.mark.skip() def test_parse_reference(): source = "x" @@ -94,7 +88,6 @@ def test_parse_reference(): # Abstract -@pytest.mark.skip() def test_parse_abstract(): source = "(\\ (x) x)" @@ -109,7 +102,6 @@ def test_parse_abstract(): # Apply -@pytest.mark.skip() def test_parse_apply_empty(): source = "(x)" @@ -123,7 +115,6 @@ def test_parse_apply_empty(): assert actual == expected -@pytest.mark.skip() def test_parse_apply_arguments(): source = "(x y z)" @@ -138,7 +129,6 @@ def test_parse_apply_arguments(): # Immediate -@pytest.mark.skip() def test_parse_immediate(): source = "42" @@ -150,7 +140,6 @@ def test_parse_immediate(): # Primitive -@pytest.mark.skip() def test_parse_add(): source = "(+ 1 2)" @@ -165,7 +154,6 @@ def test_parse_add(): assert actual == expected -@pytest.mark.skip() def test_parse_subtract(): source = "(- 3 2)" @@ -180,7 +168,6 @@ def test_parse_subtract(): assert actual == expected -@pytest.mark.skip() def test_parse_multiply(): source = "(* 2 3)" expected = Primitive( @@ -193,7 +180,6 @@ def test_parse_multiply(): # Branch -@pytest.mark.skip() def test_parse_less_than(): source = "(if (< 1 2) 1 0)" @@ -210,7 +196,6 @@ def test_parse_less_than(): assert actual == expected -@pytest.mark.skip() def test_parse_equal_to(): source = "(if (== 1 1) 1 0)" @@ -228,7 +213,6 @@ def test_parse_equal_to(): # Allocate -@pytest.mark.skip() def test_parse_allocate(): source = "(allocate 0)" @@ -242,7 +226,6 @@ def test_parse_allocate(): # Load -@pytest.mark.skip() def test_parse_load(): source = "(load x 0)" @@ -257,7 +240,6 @@ def test_parse_load(): # Store -@pytest.mark.skip() def test_parse_store(): source = "(store x 0 1)" @@ -272,7 +254,6 @@ def test_parse_store(): assert actual == expected -@pytest.mark.skip() def test_parse_begin(): source = "(begin x)" @@ -286,7 +267,6 @@ def test_parse_begin(): assert actual == expected -@pytest.mark.skip() def test_parse_begin_effects(): source = "(begin x y z)" @@ -304,7 +284,6 @@ def test_parse_begin_effects(): # Program -@pytest.mark.skip() def test_parse_program_identity(): source = "(l3 (x) x)" From 937f12d35866fee2abb7748dd59c4d1c89385efd Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Thu, 5 Mar 2026 17:23:46 -0500 Subject: [PATCH 18/35] Added most of parse logic, passing all provided tests but need to add some to hit full branch coverage --- packages/L3/src/L3/parse.py | 138 ++++++++++++++++++++++++++++++++++-- 1 file changed, 133 insertions(+), 5 deletions(-) diff --git a/packages/L3/src/L3/parse.py b/packages/L3/src/L3/parse.py index 8fbd08f..5af096c 100644 --- a/packages/L3/src/L3/parse.py +++ b/packages/L3/src/L3/parse.py @@ -5,9 +5,20 @@ from lark.visitors import v_args # pyright: ignore[reportUnknownVariableType] from .syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, Identifier, + Immediate, Let, + LetRec, + Load, + Primitive, Program, + Reference, + Store, Term, ) @@ -27,9 +38,9 @@ def program( def parameters( self, - parameters: Sequence[Identifier], + parameters: Sequence[Token], ) -> Sequence[Identifier]: - return parameters + return [str(p) for p in parameters] @v_args(inline=True) def term( @@ -57,7 +68,7 @@ def letrec( bindings: Sequence[tuple[Identifier, Term]], body: Term, ) -> Term: - return Let( + return LetRec( bindings=bindings, body=body, ) @@ -71,10 +82,127 @@ def bindings( @v_args(inline=True) def binding( self, - name: Identifier, + name: Token, value: Term, ) -> tuple[Identifier, Term]: - return name, value + return str(name), value + + @v_args(inline=True) + def reference( + self, + name: Token, + ) -> Term: + return Reference(name=str(name)) + + @v_args(inline=True) + def abstract( + self, + _lambda: Token, + parameters: Sequence[Identifier], + body: Term, + ) -> Term: + return Abstract( + parameters=parameters, + body=body, + ) + + @v_args(inline=True) + def apply( + self, + target: Term, + *arguments: Term, + ) -> Term: + return Apply( + target=target, + arguments=list(arguments), + ) + + @v_args(inline=True) + def immediate( + self, + value: Token, + ) -> Term: + return Immediate(value=int(value)) + + @v_args(inline=True) + def primitive( + self, + operator: Token, + left: Term, + right: Term, + ) -> Term: + return Primitive( + operator=str(operator), # type: ignore + left=left, + right=right, + ) + + @v_args(inline=True) + def branch( + self, + _if: Token, + operator: Token, + left: Term, + right: Term, + consequent: Term, + otherwise: Term, + ) -> Term: + return Branch( + operator=str(operator), # type: ignore + left=left, + right=right, + consequent=consequent, + otherwise=otherwise, + ) + + @v_args(inline=True) + def allocate( + self, + _allocate: Token, + count: Immediate, + ) -> Term: + return Allocate( + count=count.value, + ) + + @v_args(inline=True) + def load( + self, + _load: Token, + base: Term, + index: Immediate, + ) -> Term: + return Load( + base=base, + index=index.value, + ) + + @v_args(inline=True) + def store( + self, + _store: Token, + base: Term, + index: Immediate, + value: Term, + ) -> Term: + return Store( + base=base, + index=index.value, + value=value, + ) + + def begin( + self, + args: Sequence[Token | Term], + ) -> Term: + # Filter out Token objects (like BEGIN token), keep only Terms + terms = [arg for arg in args if not isinstance(arg, Token)] + if len(terms) == 0: + raise ValueError("begin requires at least one term") + return Begin( + effects=terms[:-1], + value=terms[-1], + ) def parse_term(source: str) -> Term: From 9f425dd97096fea629aa63d1030b107ea9990bd2 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 6 Mar 2026 13:46:42 -0500 Subject: [PATCH 19/35] Added final test for branch coverage --- packages/L3/test/L3/test_parse.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/packages/L3/test/L3/test_parse.py b/packages/L3/test/L3/test_parse.py index f48391a..e4aa85a 100644 --- a/packages/L3/test/L3/test_parse.py +++ b/packages/L3/test/L3/test_parse.py @@ -1,4 +1,5 @@ -from L3.parse import parse_program, parse_term +import pytest +from L3.parse import AstTransformer, parse_program, parse_term from L3.syntax import ( Abstract, Allocate, @@ -14,6 +15,7 @@ Reference, Store, ) +from lark import Token # Let @@ -295,3 +297,10 @@ def test_parse_program_identity(): actual = parse_program(source) assert actual == expected + + +def test_begin_requires_at_least_one_term(): + transformer = AstTransformer() + + with pytest.raises(ValueError, match="begin requires at least one term"): + transformer.begin([Token("BEGIN", "begin")]) From 3036686fe657495c6dab3d01c1d289ae95828f30 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 6 Mar 2026 14:07:43 -0500 Subject: [PATCH 20/35] skipped test for optimizer --- packages/L2/test/L2/test_optimize.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/L2/test/L2/test_optimize.py b/packages/L2/test/L2/test_optimize.py index 716e9cc..56cb31c 100644 --- a/packages/L2/test/L2/test_optimize.py +++ b/packages/L2/test/L2/test_optimize.py @@ -1,3 +1,4 @@ +import pytest from L2.optimize import optimize_program from L2.syntax import ( Immediate, @@ -6,6 +7,7 @@ ) +@pytest.mark.skip() def test_optimize_program(): program = Program( parameters=[], From 4d66127afe4856e7f3e6908b99891a209839152f Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Wed, 11 Mar 2026 22:35:42 -0400 Subject: [PATCH 21/35] Added files for optimize and did first implementation of constant folding, may need to revise. --- packages/L2/src/L2/constant_folding.py | 165 ++++++++++++++++++++ packages/L2/src/L2/constant_propogation.py | 0 packages/L2/src/L2/dead_code_elimination.py | 0 packages/L2/src/L2/main.py | 0 4 files changed, 165 insertions(+) create mode 100644 packages/L2/src/L2/constant_folding.py create mode 100644 packages/L2/src/L2/constant_propogation.py create mode 100644 packages/L2/src/L2/dead_code_elimination.py create mode 100644 packages/L2/src/L2/main.py diff --git a/packages/L2/src/L2/constant_folding.py b/packages/L2/src/L2/constant_folding.py new file mode 100644 index 0000000..684f8fc --- /dev/null +++ b/packages/L2/src/L2/constant_folding.py @@ -0,0 +1,165 @@ +from collections.abc import Mapping +from functools import partial + +from .syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, + Identifier, + Immediate, + Let, + Load, + Primitive, + Reference, + Store, + Term, +) + +type Context = Mapping[Identifier, Term] + + +def constant_folding_term( + term: Term, + context: Context, +) -> Term: + recur = partial(constant_folding_term, context=context) # noqa: F841 + + match term: + case Let(bindings=bindings, body=body): + new_bindings: list[tuple[Identifier, Term]] = [] + new_context = dict(context) + for name, value in bindings: + folded = recur(value) + new_bindings.append((name, folded)) + if isinstance(folded, Immediate | Reference): + new_context[name] = folded + return Let( + bindings=new_bindings, + body=constant_folding_term(body, new_context), + ) + + case Reference(name=name): + if name in context: + return context[name] + return term + + case Abstract(parameters=parameters, body=body): + return Abstract(parameters=parameters, body=recur(body)) + + case Apply(target=target, arguments=arguments): + return Apply( + target=recur(target), + arguments=[recur(a) for a in arguments], + ) + + case Immediate(value=_value): + return term + + case Primitive(operator=operator, left=left, right=right): + match operator: + case "+": + match recur(left), recur(right): + case Immediate(value=i1), Immediate(value=i2): + return Immediate(value=i1 + i2) + + case Immediate(value=0), right: + return right + + case [ + Primitive(operator="+", left=Immediate(value=i1), right=left), + Primitive(operator="+", left=Immediate(value=i2), right=right), + ]: + return Primitive( + operator="+", + left=Immediate(value=i1 + i2), + right=Primitive( + operator="+", + left=left, + right=right, + ), + ) + + case left, Immediate() as right: + return Primitive( + operator="+", + left=right, + right=left, + ) + + case left, right: + return Primitive( + operator="+", + left=left, + right=right, + ) + + case "-": + match recur(left), recur(right): + case Immediate(value=i1), Immediate(value=i2): + return Immediate(value=i1 - i2) + + case left, right: + return Primitive(operator="-", left=left, right=right) + + case "*": + match recur(left), recur(right): + case Immediate(value=i1), Immediate(value=i2): + return Immediate(value=i1 * i2) + + case Immediate(value=0), _: + return Immediate(value=0) + + case _, Immediate(value=0): + return Immediate(value=0) + + case Immediate(value=1), right: + return right + + case left, Immediate(value=1): + return left + + case left, Immediate() as right: + return Primitive(operator="*", left=right, right=left) + + case left, right: + return Primitive(operator="*", left=left, right=right) + + case Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + folded_left = recur(left) + folded_right = recur(right) + folded_consequent = recur(consequent) + folded_otherwise = recur(otherwise) + match operator: + case "<": + match folded_left, folded_right: + case Immediate(value=i1), Immediate(value=i2): + return folded_consequent if i1 < i2 else folded_otherwise + case _: + pass + case "==": + match folded_left, folded_right: + case Immediate(value=i1), Immediate(value=i2): + return folded_consequent if i1 == i2 else folded_otherwise + case _: + pass + return Branch( + operator=operator, + left=folded_left, + right=folded_right, + consequent=folded_consequent, + otherwise=folded_otherwise, + ) + + case Allocate(count=count): + return Allocate(count=count) + + case Load(base=base, index=index): + return Load(base=recur(base), index=index) + + case Store(base=base, index=index, value=value): + return Store(base=recur(base), index=index, value=recur(value)) + + case Begin(effects=effects, value=value): # pragma: no branch + return Begin(effects=[recur(e) for e in effects], value=recur(value)) diff --git a/packages/L2/src/L2/constant_propogation.py b/packages/L2/src/L2/constant_propogation.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/L2/src/L2/dead_code_elimination.py b/packages/L2/src/L2/dead_code_elimination.py new file mode 100644 index 0000000..e69de29 diff --git a/packages/L2/src/L2/main.py b/packages/L2/src/L2/main.py new file mode 100644 index 0000000..e69de29 From 470b5fbcadf84242396a7d3b1b5f3a9a039e5f65 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Thu, 12 Mar 2026 22:49:20 -0400 Subject: [PATCH 22/35] Wrote constant propogation and broke helper functions out into util.py --- packages/L2/src/L2/constant_folding.py | 30 +++----- packages/L2/src/L2/constant_propogation.py | 88 ++++++++++++++++++++++ packages/L2/src/L2/util.py | 45 +++++++++++ 3 files changed, 144 insertions(+), 19 deletions(-) create mode 100644 packages/L2/src/L2/util.py diff --git a/packages/L2/src/L2/constant_folding.py b/packages/L2/src/L2/constant_folding.py index 684f8fc..563e245 100644 --- a/packages/L2/src/L2/constant_folding.py +++ b/packages/L2/src/L2/constant_folding.py @@ -1,4 +1,3 @@ -from collections.abc import Mapping from functools import partial from .syntax import ( @@ -7,7 +6,6 @@ Apply, Begin, Branch, - Identifier, Immediate, Let, Load, @@ -16,8 +14,12 @@ Store, Term, ) - -type Context = Mapping[Identifier, Term] +from .util import ( + Context, + extend_context_with_bindings, + normalize_commutative_immediate_left, + recur_terms, +) def constant_folding_term( @@ -28,13 +30,7 @@ def constant_folding_term( match term: case Let(bindings=bindings, body=body): - new_bindings: list[tuple[Identifier, Term]] = [] - new_context = dict(context) - for name, value in bindings: - folded = recur(value) - new_bindings.append((name, folded)) - if isinstance(folded, Immediate | Reference): - new_context[name] = folded + new_bindings, new_context = extend_context_with_bindings(bindings, context, recur) return Let( bindings=new_bindings, body=constant_folding_term(body, new_context), @@ -51,7 +47,7 @@ def constant_folding_term( case Apply(target=target, arguments=arguments): return Apply( target=recur(target), - arguments=[recur(a) for a in arguments], + arguments=recur_terms(arguments, recur), ) case Immediate(value=_value): @@ -82,11 +78,7 @@ def constant_folding_term( ) case left, Immediate() as right: - return Primitive( - operator="+", - left=right, - right=left, - ) + return normalize_commutative_immediate_left("+", left, right) case left, right: return Primitive( @@ -121,7 +113,7 @@ def constant_folding_term( return left case left, Immediate() as right: - return Primitive(operator="*", left=right, right=left) + return normalize_commutative_immediate_left("*", left, right) case left, right: return Primitive(operator="*", left=left, right=right) @@ -162,4 +154,4 @@ def constant_folding_term( return Store(base=recur(base), index=index, value=recur(value)) case Begin(effects=effects, value=value): # pragma: no branch - return Begin(effects=[recur(e) for e in effects], value=recur(value)) + return Begin(effects=recur_terms(effects, recur), value=recur(value)) diff --git a/packages/L2/src/L2/constant_propogation.py b/packages/L2/src/L2/constant_propogation.py index e69de29..b5ea6d5 100644 --- a/packages/L2/src/L2/constant_propogation.py +++ b/packages/L2/src/L2/constant_propogation.py @@ -0,0 +1,88 @@ +from functools import partial + +from .syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, + Immediate, + Let, + Load, + Primitive, + Reference, + Store, + Term, +) +from .util import ( + Context, + extend_context_with_bindings, + recur_terms, +) + + +def constant_propogation_term( + term: Term, + context: Context, +) -> Term: + recur = partial(constant_propogation_term, context=context) + + match term: + case Let(bindings=bindings, body=body): + new_bindings, new_context = extend_context_with_bindings(bindings, context, recur) + return Let( + bindings=new_bindings, + body=constant_propogation_term(body, new_context), + ) + + case Reference(name=name): + if name in context: + return context[name] + return term + + case Abstract(parameters=parameters, body=body): + abstract_context = {name: value for name, value in context.items() if name not in parameters} + return Abstract( + parameters=parameters, + body=constant_propogation_term(body, abstract_context), + ) + + case Apply(target=target, arguments=arguments): + return Apply( + target=recur(target), + arguments=recur_terms(arguments, recur), + ) + + case Immediate(value=_value): + return term + + case Primitive(operator=operator, left=left, right=right): + return Primitive( + operator=operator, + left=recur(left), + right=recur(right), + ) + + case Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + return Branch( + operator=operator, + left=recur(left), + right=recur(right), + consequent=recur(consequent), + otherwise=recur(otherwise), + ) + + case Allocate(count=count): + return Allocate(count=count) + + case Load(base=base, index=index): + return Load(base=recur(base), index=index) + + case Store(base=base, index=index, value=value): + return Store(base=recur(base), index=index, value=recur(value)) + + case Begin(effects=effects, value=value): # pragma: no branch + return Begin( + effects=recur_terms(effects, recur), + value=recur(value), + ) diff --git a/packages/L2/src/L2/util.py b/packages/L2/src/L2/util.py new file mode 100644 index 0000000..83cdeb6 --- /dev/null +++ b/packages/L2/src/L2/util.py @@ -0,0 +1,45 @@ +from collections.abc import Callable, Sequence + +from .syntax import ( + Identifier, + Immediate, + Primitive, + Reference, + Term, +) + +type Context = dict[Identifier, Term] + + +def recur_terms( + terms: Sequence[Term], + recur: Callable[[Term], Term], +) -> list[Term]: + return [recur(term) for term in terms] + + +def extend_context_with_bindings( + bindings: Sequence[tuple[Identifier, Term]], + context: Context, + recur: Callable[[Term], Term], +) -> tuple[list[tuple[Identifier, Term]], Context]: + new_bindings: list[tuple[Identifier, Term]] = [] + new_context = dict(context) + for name, value in bindings: + result = recur(value) + new_bindings.append((name, result)) + if isinstance(result, Immediate | Reference): + new_context[name] = result + return new_bindings, new_context + + +def normalize_commutative_immediate_left( + operator: str, + left: Term, + right: Term, +) -> Primitive: + return Primitive( + operator=operator, # type: ignore[arg-type] + left=right, + right=left, + ) From 3e0fa1ea71c3b0b3e69bf73b77623fd0f479dcdb Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Mar 2026 12:44:32 -0400 Subject: [PATCH 23/35] Dead code elimination implementation --- packages/L2/src/L2/dead_code_elimination.py | 180 ++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/packages/L2/src/L2/dead_code_elimination.py b/packages/L2/src/L2/dead_code_elimination.py index e69de29..4bf2d16 100644 --- a/packages/L2/src/L2/dead_code_elimination.py +++ b/packages/L2/src/L2/dead_code_elimination.py @@ -0,0 +1,180 @@ +from functools import partial + +from .syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, + Identifier, + Immediate, + Let, + Load, + Primitive, + Reference, + Store, + Term, +) +from .util import ( + Context, + recur_terms, +) + + +def is_pure(term: Term) -> bool: + match term: + case Immediate(): + return True + + case Reference(): + return True + + case Primitive(left=left, right=right): + return is_pure(left) and is_pure(right) + + case Abstract(body=body): + return is_pure(body) + + case Let(bindings=bindings, body=body): + return all(is_pure(value) for _, value in bindings) and is_pure(body) + + case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): + return is_pure(left) and is_pure(right) and is_pure(consequent) and is_pure(otherwise) + + case Load(base=base): + return is_pure(base) + + case Begin(effects=effects, value=value): + return all(is_pure(effect) for effect in effects) and is_pure(value) + + case Apply(): + return False + + case Allocate(): + return False + + case Store(): + return False + + +def free_vars(term: Term) -> set[Identifier]: + match term: + case Immediate(): + return set() + + case Reference(name=name): + return {name} + + case Primitive(left=left, right=right): + return free_vars(left) | free_vars(right) + + case Apply(target=target, arguments=arguments): + result = free_vars(target) + for argument in arguments: + result |= free_vars(argument) + return result + + case Abstract(parameters=parameters, body=body): + return free_vars(body) - set(parameters) + + case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): + return free_vars(left) | free_vars(right) | free_vars(consequent) | free_vars(otherwise) + + case Load(base=base): + return free_vars(base) + + case Store(base=base, value=value): + return free_vars(base) | free_vars(value) + + case Begin(effects=effects, value=value): + result = free_vars(value) + for effect in effects: + result |= free_vars(effect) + return result + + case Allocate(): + return set() + + case Let(bindings=bindings, body=body): + names = [name for name, _ in bindings] + result = free_vars(body) - set(names) + for _, value in bindings: + result |= free_vars(value) + return result + + +def dead_code_elimination_term( + term: Term, + context: Context, +) -> Term: + recur = partial(dead_code_elimination_term, context=context) + + match term: + case Let(bindings=bindings, body=body): + new_values = [(name, recur(value)) for name, value in bindings] + new_body = recur(body) + + live = free_vars(new_body) + kept_reversed: list[tuple[Identifier, Term]] = [] + + for name, value in reversed(new_values): + if name in live or not is_pure(value): + kept_reversed.append((name, value)) + live.discard(name) + live |= free_vars(value) + + kept = list(reversed(kept_reversed)) + if len(kept) == 0: + return new_body + + return Let(bindings=kept, body=new_body) + + case Reference(name=_name): + return term + + case Abstract(parameters=parameters, body=body): + return Abstract(parameters=parameters, body=recur(body)) + + case Apply(target=target, arguments=arguments): + return Apply( + target=recur(target), + arguments=recur_terms(arguments, recur), + ) + + case Immediate(value=_value): + return term + + case Primitive(operator=operator, left=left, right=right): + return Primitive( + operator=operator, + left=recur(left), + right=recur(right), + ) + + case Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + return Branch( + operator=operator, + left=recur(left), + right=recur(right), + consequent=recur(consequent), + otherwise=recur(otherwise), + ) + + case Allocate(count=count): + return Allocate(count=count) + + case Load(base=base, index=index): + return Load(base=recur(base), index=index) + + case Store(base=base, index=index, value=value): + return Store(base=recur(base), index=index, value=recur(value)) + + case Begin(effects=effects, value=value): # pragma: no branch + new_effects = [recur(effect) for effect in effects] + kept_effects = [effect for effect in new_effects if not is_pure(effect)] + new_value = recur(value) + + if len(kept_effects) == 0: + return new_value + + return Begin(effects=kept_effects, value=new_value) From cbd4dad64f6ecdf518ac45f64eecec0a4defb3f1 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Mar 2026 12:45:51 -0400 Subject: [PATCH 24/35] Commented out file --- packages/L2/src/L2/cps_convert.py | 118 +++++++++++++++--------------- 1 file changed, 59 insertions(+), 59 deletions(-) diff --git a/packages/L2/src/L2/cps_convert.py b/packages/L2/src/L2/cps_convert.py index ee729ad..62eddb8 100644 --- a/packages/L2/src/L2/cps_convert.py +++ b/packages/L2/src/L2/cps_convert.py @@ -1,82 +1,82 @@ -from collections.abc import Callable, Sequence -from functools import partial +# from collections.abc import Callable, Sequence +# from functools import partial -from L1 import syntax as L1 +# from L1 import syntax as L1 -from L2 import syntax as L2 +# from L2 import syntax as L2 -def cps_convert_term( - term: L2.Term, - k: Callable[[L1.Identifier], L1.Statement], - fresh: Callable[[str], str], -) -> L1.Statement: - _term = partial(cps_convert_term, fresh=fresh) - _terms = partial(cps_convert_terms, fresh=fresh) +# def cps_convert_term( +# term: L2.Term, +# k: Callable[[L1.Identifier], L1.Statement], +# fresh: Callable[[str], str], +# ) -> L1.Statement: +# _term = partial(cps_convert_term, fresh=fresh) +# _terms = partial(cps_convert_terms, fresh=fresh) - match term: - case L2.Let(bindings=bindings, body=body): - pass +# match term: +# case L2.Let(bindings=bindings, body=body): +# pass - case L2.Reference(name=name): - pass +# case L2.Reference(name=name): +# pass - case L2.Abstract(parameters=parameters, body=body): - pass +# case L2.Abstract(parameters=parameters, body=body): +# pass - case L2.Apply(target=target, arguments=arguments): - pass +# case L2.Apply(target=target, arguments=arguments): +# pass - case L2.Immediate(value=value): - pass +# case L2.Immediate(value=value): +# pass - case L2.Primitive(operator=operator, left=left, right=right): - pass +# case L2.Primitive(operator=operator, left=left, right=right): +# pass - case L2.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): - pass +# case L2.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): +# pass - case L2.Allocate(count=count): - pass +# case L2.Allocate(count=count): +# pass - case L2.Load(base=base, index=index): - pass +# case L2.Load(base=base, index=index): +# pass - case L2.Store(base=base, index=index, value=value): - pass +# case L2.Store(base=base, index=index, value=value): +# pass - case L2.Begin(effects=effects, value=value): # pragma: no branch - pass +# case L2.Begin(effects=effects, value=value): # pragma: no branch +# pass -def cps_convert_terms( - terms: Sequence[L2.Term], - k: Callable[[Sequence[L1.Identifier]], L1.Statement], - fresh: Callable[[str], str], -) -> L1.Statement: - _term = partial(cps_convert_term, fresh=fresh) - _terms = partial(cps_convert_terms, fresh=fresh) +# def cps_convert_terms( +# terms: Sequence[L2.Term], +# k: Callable[[Sequence[L1.Identifier]], L1.Statement], +# fresh: Callable[[str], str], +# ) -> L1.Statement: +# _term = partial(cps_convert_term, fresh=fresh) +# _terms = partial(cps_convert_terms, fresh=fresh) - match terms: - case []: - return k([]) +# match terms: +# case []: +# return k([]) - case [first, *rest]: - return _term(first, lambda first: _terms(rest, lambda rest: k([first, *rest]))) +# case [first, *rest]: +# return _term(first, lambda first: _terms(rest, lambda rest: k([first, *rest]))) - case _: # pragma: no cover - raise ValueError(terms) +# case _: # pragma: no cover +# raise ValueError(terms) -def cps_convert_program( - program: L2.Program, - fresh: Callable[[str], str], -) -> L1.Program: - _term = partial(cps_convert_term, fresh=fresh) +# def cps_convert_program( +# program: L2.Program, +# fresh: Callable[[str], str], +# ) -> L1.Program: +# _term = partial(cps_convert_term, fresh=fresh) - match program: - case L2.Program(parameters=parameters, body=body): # pragma: no branch - return L1.Program( - parameters=parameters, - body=_term(body, lambda value: L1.Halt(value=value)), - ) +# match program: +# case L2.Program(parameters=parameters, body=body): # pragma: no branch +# return L1.Program( +# parameters=parameters, +# body=_term(body, lambda value: L1.Halt(value=value)), +# ) From 8abe939d7cf720478e1835563780427f8e2587ed Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Mar 2026 14:06:42 -0400 Subject: [PATCH 25/35] classwork with optimize implementation in main.py --- packages/L2/src/L2/main.py | 32 ++ packages/L2/src/L2/optimize.py | 7 - packages/L2/test/L2/test_cps_convert.py | 444 ++++++++++++------------ packages/L2/test/L2/test_optimize.py | 2 +- packages/L3/src/L3/main.py | 13 +- packages/L3/src/L3/uniqify.py | 3 +- 6 files changed, 264 insertions(+), 237 deletions(-) delete mode 100644 packages/L2/src/L2/optimize.py diff --git a/packages/L2/src/L2/main.py b/packages/L2/src/L2/main.py index e69de29..b017553 100644 --- a/packages/L2/src/L2/main.py +++ b/packages/L2/src/L2/main.py @@ -0,0 +1,32 @@ +from .constant_folding import constant_folding_term +from .constant_propogation import constant_propogation_term +from .dead_code_elimination import dead_code_elimination_term +from .syntax import Program + + +def optimize_program_step( + program: Program, +) -> tuple[Program, bool]: + propagated = Program( + parameters=program.parameters, + body=constant_propogation_term(program.body, {}), + ) + folded = Program( + parameters=propagated.parameters, + body=constant_folding_term(propagated.body, {}), + ) + eliminated = Program( + parameters=folded.parameters, + body=dead_code_elimination_term(folded.body, {}), + ) + return eliminated, eliminated != program + + +def main( + program: Program, +) -> Program: + current = program + while True: + current, changed = optimize_program_step(current) + if not changed: + return current diff --git a/packages/L2/src/L2/optimize.py b/packages/L2/src/L2/optimize.py deleted file mode 100644 index 77ef7c6..0000000 --- a/packages/L2/src/L2/optimize.py +++ /dev/null @@ -1,7 +0,0 @@ -from .syntax import Program - - -def optimize_program( - program: Program, -) -> Program: - return program diff --git a/packages/L2/test/L2/test_cps_convert.py b/packages/L2/test/L2/test_cps_convert.py index 1d47e2a..cf45d29 100644 --- a/packages/L2/test/L2/test_cps_convert.py +++ b/packages/L2/test/L2/test_cps_convert.py @@ -1,247 +1,247 @@ -from L1 import syntax as L1 -from L2 import syntax as L2 -from L2.cps_convert import cps_convert_program, cps_convert_term -from util.sequential_name_generator import SequentialNameGenerator +# from L1 import syntax as L1 +# from L2 import syntax as L2 +# from L2.cps_convert import cps_convert_program, cps_convert_term +# from util.sequential_name_generator import SequentialNameGenerator -def k(v: L1.Identifier) -> L1.Statement: - return L1.Halt(value=v) +# def k(v: L1.Identifier) -> L1.Statement: +# return L1.Halt(value=v) -def test_cps_convert_term_name(): - term = L2.Reference(name="x") +# def test_cps_convert_term_name(): +# term = L2.Reference(name="x") - fresh = SequentialNameGenerator() +# fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) +# actual = cps_convert_term(term, k, fresh) - expected = L1.Halt(value="x") - assert actual == expected +# expected = L1.Halt(value="x") +# assert actual == expected -def test_cps_convert_term_immediate(): - term = L2.Immediate(value=42) +# def test_cps_convert_term_immediate(): +# term = L2.Immediate(value=42) - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) - expected = L1.Immediate( - destination="t0", - value=42, - then=L1.Halt(value="t0"), - ) +# expected = L1.Immediate( +# destination="t0", +# value=42, +# then=L1.Halt(value="t0"), +# ) - assert actual == expected +# assert actual == expected -def test_cps_convert_term_primitive(): - term = L2.Primitive( - operator="+", - left=L2.Reference(name="x"), - right=L2.Reference(name="y"), - ) +# def test_cps_convert_term_primitive(): +# term = L2.Primitive( +# operator="+", +# left=L2.Reference(name="x"), +# right=L2.Reference(name="y"), +# ) - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) - expected = L1.Primitive( - destination="t0", - operator="+", - left="x", - right="y", - then=L1.Halt(value="t0"), - ) - - assert actual == expected - - -def test_cps_convert_term_let(): - term = L2.Let( - bindings=[ - ("a", L2.Reference(name="x")), - ("b", L2.Reference(name="y")), - ], - body=L2.Reference(name="b"), - ) +# expected = L1.Primitive( +# destination="t0", +# operator="+", +# left="x", +# right="y", +# then=L1.Halt(value="t0"), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_let(): +# term = L2.Let( +# bindings=[ +# ("a", L2.Reference(name="x")), +# ("b", L2.Reference(name="y")), +# ], +# body=L2.Reference(name="b"), +# ) - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Copy( - destination="a", - source="x", - then=L1.Copy( - destination="b", - source="y", - then=L1.Halt(value="b"), - ), - ) +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Copy( +# destination="a", +# source="x", +# then=L1.Copy( +# destination="b", +# source="y", +# then=L1.Halt(value="b"), +# ), +# ) - assert actual == expected +# assert actual == expected -def test_cps_convert_term_abstract(): - term = L2.Abstract( - parameters=["x"], - body=L2.Reference(name="x"), - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) +# def test_cps_convert_term_abstract(): +# term = L2.Abstract( +# parameters=["x"], +# body=L2.Reference(name="x"), +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) - expected = L1.Abstract( - destination="t0", - parameters=["x", "k0"], - body=L1.Apply(target="k0", arguments=["x"]), - then=L1.Halt(value="t0"), - ) +# expected = L1.Abstract( +# destination="t0", +# parameters=["x", "k0"], +# body=L1.Apply(target="k0", arguments=["x"]), +# then=L1.Halt(value="t0"), +# ) - assert actual == expected +# assert actual == expected -def test_cps_convert_term_apply(): - term = L2.Apply( - target=L2.Reference(name="f"), - arguments=[ - L2.Reference(name="y"), - ], - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Abstract( - destination="k0", - parameters=["t0"], - body=L1.Halt(value="t0"), - then=L1.Apply( - target="f", - arguments=["y", "k0"], - ), - ) - - assert actual == expected - - -def test_cps_convert_term_branch(): - term = L2.Branch( - operator="==", - left=L2.Reference(name="x"), - right=L2.Reference(name="y"), - consequent=L2.Reference(name="a"), - otherwise=L2.Reference(name="b"), - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Abstract( - destination="j0", - parameters=["t0"], - body=L1.Halt(value="t0"), - then=L1.Branch( - operator="==", - left="x", - right="y", - then=L1.Apply( - target="j0", - arguments=["a"], - ), - otherwise=L1.Apply( - target="j0", - arguments=["b"], - ), - ), - ) - - assert actual == expected - - -def test_cps_convert_term_allocate(): - term = L2.Allocate(count=0) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Allocate( - destination="t0", - count=0, - then=L1.Halt(value="t0"), - ) - - assert actual == expected - - -def test_cps_convert_term_load(): - term_load = L2.Load( - base=L2.Reference(name="x"), - index=0, - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term_load, k, fresh) - - expected = L1.Load( - destination="t0", - base="x", - index=0, - then=L1.Halt(value="t0"), - ) - - assert actual == expected - - -def test_cps_convert_term_store(): - term = L2.Store( - base=L2.Reference(name="x"), - index=0, - value=L2.Reference(name="y"), - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Store( - base="x", - index=0, - value="y", - then=L1.Immediate( - destination="t0", - value=0, - then=L1.Halt(value="t0"), - ), - ) - - assert actual == expected - - -def test_cps_convert_term_begin(): - term = L2.Begin( - effects=[ - L2.Reference(name="x"), - ], - value=L2.Reference(name="y"), - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_term(term, k, fresh) - - expected = L1.Halt(value="y") - assert actual == expected - - -def test_cps_convert_program(): - program = L2.Program( - parameters=["x"], - body=L2.Reference(name="x"), - ) - - fresh = SequentialNameGenerator() - actual = cps_convert_program(program, fresh) - - expected = L1.Program( - parameters=["x"], - body=L1.Halt(value="x"), - ) +# def test_cps_convert_term_apply(): +# term = L2.Apply( +# target=L2.Reference(name="f"), +# arguments=[ +# L2.Reference(name="y"), +# ], +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Abstract( +# destination="k0", +# parameters=["t0"], +# body=L1.Halt(value="t0"), +# then=L1.Apply( +# target="f", +# arguments=["y", "k0"], +# ), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_branch(): +# term = L2.Branch( +# operator="==", +# left=L2.Reference(name="x"), +# right=L2.Reference(name="y"), +# consequent=L2.Reference(name="a"), +# otherwise=L2.Reference(name="b"), +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Abstract( +# destination="j0", +# parameters=["t0"], +# body=L1.Halt(value="t0"), +# then=L1.Branch( +# operator="==", +# left="x", +# right="y", +# then=L1.Apply( +# target="j0", +# arguments=["a"], +# ), +# otherwise=L1.Apply( +# target="j0", +# arguments=["b"], +# ), +# ), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_allocate(): +# term = L2.Allocate(count=0) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Allocate( +# destination="t0", +# count=0, +# then=L1.Halt(value="t0"), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_load(): +# term_load = L2.Load( +# base=L2.Reference(name="x"), +# index=0, +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term_load, k, fresh) + +# expected = L1.Load( +# destination="t0", +# base="x", +# index=0, +# then=L1.Halt(value="t0"), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_store(): +# term = L2.Store( +# base=L2.Reference(name="x"), +# index=0, +# value=L2.Reference(name="y"), +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Store( +# base="x", +# index=0, +# value="y", +# then=L1.Immediate( +# destination="t0", +# value=0, +# then=L1.Halt(value="t0"), +# ), +# ) + +# assert actual == expected + + +# def test_cps_convert_term_begin(): +# term = L2.Begin( +# effects=[ +# L2.Reference(name="x"), +# ], +# value=L2.Reference(name="y"), +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_term(term, k, fresh) + +# expected = L1.Halt(value="y") +# assert actual == expected + + +# def test_cps_convert_program(): +# program = L2.Program( +# parameters=["x"], +# body=L2.Reference(name="x"), +# ) + +# fresh = SequentialNameGenerator() +# actual = cps_convert_program(program, fresh) + +# expected = L1.Program( +# parameters=["x"], +# body=L1.Halt(value="x"), +# ) - assert actual == expected +# assert actual == expected diff --git a/packages/L2/test/L2/test_optimize.py b/packages/L2/test/L2/test_optimize.py index 56cb31c..5a3767a 100644 --- a/packages/L2/test/L2/test_optimize.py +++ b/packages/L2/test/L2/test_optimize.py @@ -1,5 +1,5 @@ import pytest -from L2.optimize import optimize_program +from L2.main import main as optimize_program from L2.syntax import ( Immediate, Primitive, diff --git a/packages/L3/src/L3/main.py b/packages/L3/src/L3/main.py index aaa9c6f..420f337 100644 --- a/packages/L3/src/L3/main.py +++ b/packages/L3/src/L3/main.py @@ -1,9 +1,10 @@ from pathlib import Path import click -from L1.to_python import to_ast_program -from L2.cps_convert import cps_convert_program -from L2.optimize import optimize_program + +# from L2.cps_convert import cps_convert_program +from L2.main import main as optimize_program +from L2.to_python import to_ast_program from .check import check_program from .eliminate_letrec import eliminate_letrec_program @@ -51,15 +52,15 @@ def main( if check: check_program(l3) - fresh, l3 = uniqify_program(l3) + fresh, l3 = uniqify_program(l3) # type: ignore l2 = eliminate_letrec_program(l3) if optimize: l2 = optimize_program(l2) - l1 = cps_convert_program(l2, fresh) + # l1 = cps_convert_program(l2, fresh) - module = to_ast_program(l1) + module = to_ast_program(l2) (output or input.with_suffix(".py")).write_text(module) diff --git a/packages/L3/src/L3/uniqify.py b/packages/L3/src/L3/uniqify.py index 2e242d6..f70c936 100644 --- a/packages/L3/src/L3/uniqify.py +++ b/packages/L3/src/L3/uniqify.py @@ -9,6 +9,7 @@ Apply, Begin, Branch, + Identifier, Immediate, Let, LetRec, @@ -20,7 +21,7 @@ Term, ) -type Context = Mapping[str, str] +type Context = Mapping[Identifier, Identifier] def uniqify_term( From 185f6fabd7b287b2fba19c6759442fe4664f438d Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Mar 2026 14:16:01 -0400 Subject: [PATCH 26/35] Cleaned up util helpers after class conversation --- packages/L2/src/L2/constant_folding.py | 17 ++++++-- packages/L2/src/L2/dead_code_elimination.py | 46 ++++++++++----------- packages/L2/src/L2/util.py | 13 ------ packages/L3/src/L3/uniqify.py | 23 ++++++++--- 4 files changed, 55 insertions(+), 44 deletions(-) diff --git a/packages/L2/src/L2/constant_folding.py b/packages/L2/src/L2/constant_folding.py index 563e245..4589abe 100644 --- a/packages/L2/src/L2/constant_folding.py +++ b/packages/L2/src/L2/constant_folding.py @@ -17,11 +17,22 @@ from .util import ( Context, extend_context_with_bindings, - normalize_commutative_immediate_left, recur_terms, ) +def _normalize_commutative_immediate_left( + operator: str, + left: Term, + right: Term, +) -> Primitive: + return Primitive( + operator=operator, # type: ignore[arg-type] + left=right, + right=left, + ) + + def constant_folding_term( term: Term, context: Context, @@ -78,7 +89,7 @@ def constant_folding_term( ) case left, Immediate() as right: - return normalize_commutative_immediate_left("+", left, right) + return _normalize_commutative_immediate_left("+", left, right) case left, right: return Primitive( @@ -113,7 +124,7 @@ def constant_folding_term( return left case left, Immediate() as right: - return normalize_commutative_immediate_left("*", left, right) + return _normalize_commutative_immediate_left("*", left, right) case left, right: return Primitive(operator="*", left=left, right=right) diff --git a/packages/L2/src/L2/dead_code_elimination.py b/packages/L2/src/L2/dead_code_elimination.py index 4bf2d16..699d419 100644 --- a/packages/L2/src/L2/dead_code_elimination.py +++ b/packages/L2/src/L2/dead_code_elimination.py @@ -21,7 +21,7 @@ ) -def is_pure(term: Term) -> bool: +def _is_pure(term: Term) -> bool: match term: case Immediate(): return True @@ -30,22 +30,22 @@ def is_pure(term: Term) -> bool: return True case Primitive(left=left, right=right): - return is_pure(left) and is_pure(right) + return _is_pure(left) and _is_pure(right) case Abstract(body=body): - return is_pure(body) + return _is_pure(body) case Let(bindings=bindings, body=body): - return all(is_pure(value) for _, value in bindings) and is_pure(body) + return all(_is_pure(value) for _, value in bindings) and _is_pure(body) case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): - return is_pure(left) and is_pure(right) and is_pure(consequent) and is_pure(otherwise) + return _is_pure(left) and _is_pure(right) and _is_pure(consequent) and _is_pure(otherwise) case Load(base=base): - return is_pure(base) + return _is_pure(base) case Begin(effects=effects, value=value): - return all(is_pure(effect) for effect in effects) and is_pure(value) + return all(_is_pure(effect) for effect in effects) and _is_pure(value) case Apply(): return False @@ -57,7 +57,7 @@ def is_pure(term: Term) -> bool: return False -def free_vars(term: Term) -> set[Identifier]: +def _free_vars(term: Term) -> set[Identifier]: match term: case Immediate(): return set() @@ -66,30 +66,30 @@ def free_vars(term: Term) -> set[Identifier]: return {name} case Primitive(left=left, right=right): - return free_vars(left) | free_vars(right) + return _free_vars(left) | _free_vars(right) case Apply(target=target, arguments=arguments): - result = free_vars(target) + result = _free_vars(target) for argument in arguments: - result |= free_vars(argument) + result |= _free_vars(argument) return result case Abstract(parameters=parameters, body=body): - return free_vars(body) - set(parameters) + return _free_vars(body) - set(parameters) case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): - return free_vars(left) | free_vars(right) | free_vars(consequent) | free_vars(otherwise) + return _free_vars(left) | _free_vars(right) | _free_vars(consequent) | _free_vars(otherwise) case Load(base=base): - return free_vars(base) + return _free_vars(base) case Store(base=base, value=value): - return free_vars(base) | free_vars(value) + return _free_vars(base) | _free_vars(value) case Begin(effects=effects, value=value): - result = free_vars(value) + result = _free_vars(value) for effect in effects: - result |= free_vars(effect) + result |= _free_vars(effect) return result case Allocate(): @@ -97,9 +97,9 @@ def free_vars(term: Term) -> set[Identifier]: case Let(bindings=bindings, body=body): names = [name for name, _ in bindings] - result = free_vars(body) - set(names) + result = _free_vars(body) - set(names) for _, value in bindings: - result |= free_vars(value) + result |= _free_vars(value) return result @@ -114,14 +114,14 @@ def dead_code_elimination_term( new_values = [(name, recur(value)) for name, value in bindings] new_body = recur(body) - live = free_vars(new_body) + live = _free_vars(new_body) kept_reversed: list[tuple[Identifier, Term]] = [] for name, value in reversed(new_values): - if name in live or not is_pure(value): + if name in live or not _is_pure(value): kept_reversed.append((name, value)) live.discard(name) - live |= free_vars(value) + live |= _free_vars(value) kept = list(reversed(kept_reversed)) if len(kept) == 0: @@ -171,7 +171,7 @@ def dead_code_elimination_term( case Begin(effects=effects, value=value): # pragma: no branch new_effects = [recur(effect) for effect in effects] - kept_effects = [effect for effect in new_effects if not is_pure(effect)] + kept_effects = [effect for effect in new_effects if not _is_pure(effect)] new_value = recur(value) if len(kept_effects) == 0: diff --git a/packages/L2/src/L2/util.py b/packages/L2/src/L2/util.py index 83cdeb6..2dbfff9 100644 --- a/packages/L2/src/L2/util.py +++ b/packages/L2/src/L2/util.py @@ -3,7 +3,6 @@ from .syntax import ( Identifier, Immediate, - Primitive, Reference, Term, ) @@ -31,15 +30,3 @@ def extend_context_with_bindings( if isinstance(result, Immediate | Reference): new_context[name] = result return new_bindings, new_context - - -def normalize_commutative_immediate_left( - operator: str, - left: Term, - right: Term, -) -> Primitive: - return Primitive( - operator=operator, # type: ignore[arg-type] - left=right, - right=left, - ) diff --git a/packages/L3/src/L3/uniqify.py b/packages/L3/src/L3/uniqify.py index f70c936..a7ceb3f 100644 --- a/packages/L3/src/L3/uniqify.py +++ b/packages/L3/src/L3/uniqify.py @@ -48,16 +48,26 @@ def uniqify_term( pass case Immediate(): - pass + return term case Primitive(operator=operator, left=left, right=right): - pass + return Primitive( + operator=operator, + left=_term(left), + right=_term(right), + ) case Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): - pass + return Branch( + operator=operator, + left=_term(left), + right=_term(right), + consequent=_term(consequent), + otherwise=_term(otherwise), + ) case Allocate(): - pass + return term case Load(base=base, index=index): pass @@ -66,7 +76,10 @@ def uniqify_term( pass case Begin(effects=effects, value=value): # pragma: no branch - pass + return Begin( + effects=[_term(effect) for effect in effects], + value=_term(value), + ) def uniqify_program( From 069766271e61886953f80b204ba322572be32756 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 13 Mar 2026 18:49:43 -0400 Subject: [PATCH 27/35] added some uniquify logic and a few optimize tests --- packages/L2/test/L2/test_optimize.py | 68 +++++++++++++++++++++++++++- packages/L3/src/L3/uniqify.py | 54 +++++++++++++++++++--- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/packages/L2/test/L2/test_optimize.py b/packages/L2/test/L2/test_optimize.py index 5a3767a..e4908fb 100644 --- a/packages/L2/test/L2/test_optimize.py +++ b/packages/L2/test/L2/test_optimize.py @@ -1,4 +1,3 @@ -import pytest from L2.main import main as optimize_program from L2.syntax import ( Immediate, @@ -7,7 +6,6 @@ ) -@pytest.mark.skip() def test_optimize_program(): program = Program( parameters=[], @@ -26,3 +24,69 @@ def test_optimize_program(): actual = optimize_program(program) assert actual == expected + + +def test_optimize_program_no_optimization(): + program = Program( + parameters=[], + body=Primitive( + operator="+", + left=Immediate(value=1), + right=Immediate(value=2), + ), + ) + + expected = program + + actual = optimize_program(program) + + assert actual == expected + + +def test_optimize_program_nested(): + program = Program( + parameters=[], + body=Primitive( + operator="+", + left=Primitive( + operator="+", + left=Immediate(value=1), + right=Immediate(value=1), + ), + right=Immediate(value=1), + ), + ) + + expected = Program( + parameters=[], + body=Primitive( + operator="+", + left=Immediate(value=2), + right=Immediate(value=1), + ), + ) + + actual = optimize_program(program) + + assert actual == expected + + +def test_optimize_program_no_optimization_nested(): + program = Program( + parameters=[], + body=Primitive( + operator="+", + left=Primitive( + operator="+", + left=Immediate(value=1), + right=Immediate(value=2), + ), + right=Immediate(value=1), + ), + ) + + expected = program + + actual = optimize_program(program) + + assert actual == expected diff --git a/packages/L3/src/L3/uniqify.py b/packages/L3/src/L3/uniqify.py index a7ceb3f..d553499 100644 --- a/packages/L3/src/L3/uniqify.py +++ b/packages/L3/src/L3/uniqify.py @@ -33,19 +33,52 @@ def uniqify_term( match term: case Let(bindings=bindings, body=body): - pass + new_bindings: list[tuple[Identifier, Term]] = [] + new_context = context + for name, value in bindings: + new_name = fresh(name) + new_bindings.append((new_name, _term(value))) + new_context = {**new_context, name: new_name} + + return Let( + bindings=new_bindings, + body=_term(body, new_context), + ) case LetRec(bindings=bindings, body=body): - pass + new_bindings: list[tuple[Identifier, Term]] = [] + new_context = context + for name, value in bindings: + new_name = fresh(name) + new_bindings.append((new_name, value)) + new_context = {**new_context, name: new_name} + + return LetRec( + bindings=[(new_name, _term(value)) for new_name, value in new_bindings], + body=_term(body, new_context), + ) case Reference(name=name): - pass + if name in context: + return Reference(name=context[name]) + return term case Abstract(parameters=parameters, body=body): - pass + new_parameters = [fresh(parameter) for parameter in parameters] + new_context = { + **context, + **{parameter: new_parameter for parameter, new_parameter in zip(parameters, new_parameters)}, + } + return Abstract( + parameters=new_parameters, + body=_term(body, new_context), + ) case Apply(target=target, arguments=arguments): - pass + return Apply( + target=_term(target), + arguments=[_term(argument) for argument in arguments], + ) case Immediate(): return term @@ -70,10 +103,17 @@ def uniqify_term( return term case Load(base=base, index=index): - pass + return Load( + base=_term(base), + index=index, + ) case Store(base=base, index=index, value=value): - pass + return Store( + base=_term(base), + index=index, + value=_term(value), + ) case Begin(effects=effects, value=value): # pragma: no branch return Begin( From f7323d34a57ab543ad772538f015b5cefee203a3 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Sat, 14 Mar 2026 21:26:13 -0400 Subject: [PATCH 28/35] went back to having optimize.py instead of matching arcitecture of previous levels --- packages/L2/src/L2/{main.py => optimize.py} | 2 +- packages/L2/test/L2/test_optimize.py | 2 +- packages/L3/src/L3/main.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename packages/L2/src/L2/{main.py => optimize.py} (97%) diff --git a/packages/L2/src/L2/main.py b/packages/L2/src/L2/optimize.py similarity index 97% rename from packages/L2/src/L2/main.py rename to packages/L2/src/L2/optimize.py index b017553..6972975 100644 --- a/packages/L2/src/L2/main.py +++ b/packages/L2/src/L2/optimize.py @@ -22,7 +22,7 @@ def optimize_program_step( return eliminated, eliminated != program -def main( +def optimize_program( program: Program, ) -> Program: current = program diff --git a/packages/L2/test/L2/test_optimize.py b/packages/L2/test/L2/test_optimize.py index e4908fb..524a6d1 100644 --- a/packages/L2/test/L2/test_optimize.py +++ b/packages/L2/test/L2/test_optimize.py @@ -1,4 +1,4 @@ -from L2.main import main as optimize_program +from L2.optimize import optimize_program from L2.syntax import ( Immediate, Primitive, diff --git a/packages/L3/src/L3/main.py b/packages/L3/src/L3/main.py index 420f337..d623913 100644 --- a/packages/L3/src/L3/main.py +++ b/packages/L3/src/L3/main.py @@ -3,7 +3,7 @@ import click # from L2.cps_convert import cps_convert_program -from L2.main import main as optimize_program +from L2.optimize import optimize_program from L2.to_python import to_ast_program from .check import check_program From 002418d4d6372fac2475141004240bb6daa03ccd Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Sun, 15 Mar 2026 18:14:36 -0400 Subject: [PATCH 29/35] I updated constant folding and dead code elimination, working through tests now. --- packages/L2/src/L2/constant_folding.py | 22 +++++++--- packages/L2/src/L2/dead_code_elimination.py | 46 ++++++++++----------- 2 files changed, 40 insertions(+), 28 deletions(-) diff --git a/packages/L2/src/L2/constant_folding.py b/packages/L2/src/L2/constant_folding.py index 4589abe..c360fd8 100644 --- a/packages/L2/src/L2/constant_folding.py +++ b/packages/L2/src/L2/constant_folding.py @@ -91,7 +91,9 @@ def constant_folding_term( case left, Immediate() as right: return _normalize_commutative_immediate_left("+", left, right) - case left, right: + # Coverage reports a synthetic exit arc on this fallback match arm. + # The arm is intentionally reachable and returns the non-folded primitive. + case left, right: # pragma: no branch return Primitive( operator="+", left=left, @@ -103,10 +105,14 @@ def constant_folding_term( case Immediate(value=i1), Immediate(value=i2): return Immediate(value=i1 - i2) - case left, right: + # Coverage reports a synthetic exit arc on this fallback match arm. + # The arm is intentionally reachable and returns the non-folded primitive. + case left, right: # pragma: no branch return Primitive(operator="-", left=left, right=right) - case "*": + # Coverage may report an extra arc on this literal case label under pattern matching. + # Runtime terms validated by the syntax model still follow normal folding logic below. + case "*": # pragma: no branch match recur(left), recur(right): case Immediate(value=i1), Immediate(value=i2): return Immediate(value=i1 * i2) @@ -126,7 +132,9 @@ def constant_folding_term( case left, Immediate() as right: return _normalize_commutative_immediate_left("*", left, right) - case left, right: + # Coverage reports a synthetic exit arc on this fallback match arm. + # The arm is intentionally reachable and returns the non-folded primitive. + case left, right: # pragma: no branch return Primitive(operator="*", left=left, right=right) case Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): @@ -141,7 +149,9 @@ def constant_folding_term( return folded_consequent if i1 < i2 else folded_otherwise case _: pass - case "==": + # Coverage may report an extra arc on this literal case label under pattern matching. + # Runtime terms validated by the syntax model use only "<" and "==". + case "==": # pragma: no branch match folded_left, folded_right: case Immediate(value=i1), Immediate(value=i2): return folded_consequent if i1 == i2 else folded_otherwise @@ -164,5 +174,7 @@ def constant_folding_term( case Store(base=base, index=index, value=value): return Store(base=recur(base), index=index, value=recur(value)) + # Coverage may report an extra structural arc for this match arm. + # Semantically this always returns the reconstructed Begin node. case Begin(effects=effects, value=value): # pragma: no branch return Begin(effects=recur_terms(effects, recur), value=recur(value)) diff --git a/packages/L2/src/L2/dead_code_elimination.py b/packages/L2/src/L2/dead_code_elimination.py index 699d419..4bf2d16 100644 --- a/packages/L2/src/L2/dead_code_elimination.py +++ b/packages/L2/src/L2/dead_code_elimination.py @@ -21,7 +21,7 @@ ) -def _is_pure(term: Term) -> bool: +def is_pure(term: Term) -> bool: match term: case Immediate(): return True @@ -30,22 +30,22 @@ def _is_pure(term: Term) -> bool: return True case Primitive(left=left, right=right): - return _is_pure(left) and _is_pure(right) + return is_pure(left) and is_pure(right) case Abstract(body=body): - return _is_pure(body) + return is_pure(body) case Let(bindings=bindings, body=body): - return all(_is_pure(value) for _, value in bindings) and _is_pure(body) + return all(is_pure(value) for _, value in bindings) and is_pure(body) case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): - return _is_pure(left) and _is_pure(right) and _is_pure(consequent) and _is_pure(otherwise) + return is_pure(left) and is_pure(right) and is_pure(consequent) and is_pure(otherwise) case Load(base=base): - return _is_pure(base) + return is_pure(base) case Begin(effects=effects, value=value): - return all(_is_pure(effect) for effect in effects) and _is_pure(value) + return all(is_pure(effect) for effect in effects) and is_pure(value) case Apply(): return False @@ -57,7 +57,7 @@ def _is_pure(term: Term) -> bool: return False -def _free_vars(term: Term) -> set[Identifier]: +def free_vars(term: Term) -> set[Identifier]: match term: case Immediate(): return set() @@ -66,30 +66,30 @@ def _free_vars(term: Term) -> set[Identifier]: return {name} case Primitive(left=left, right=right): - return _free_vars(left) | _free_vars(right) + return free_vars(left) | free_vars(right) case Apply(target=target, arguments=arguments): - result = _free_vars(target) + result = free_vars(target) for argument in arguments: - result |= _free_vars(argument) + result |= free_vars(argument) return result case Abstract(parameters=parameters, body=body): - return _free_vars(body) - set(parameters) + return free_vars(body) - set(parameters) case Branch(left=left, right=right, consequent=consequent, otherwise=otherwise): - return _free_vars(left) | _free_vars(right) | _free_vars(consequent) | _free_vars(otherwise) + return free_vars(left) | free_vars(right) | free_vars(consequent) | free_vars(otherwise) case Load(base=base): - return _free_vars(base) + return free_vars(base) case Store(base=base, value=value): - return _free_vars(base) | _free_vars(value) + return free_vars(base) | free_vars(value) case Begin(effects=effects, value=value): - result = _free_vars(value) + result = free_vars(value) for effect in effects: - result |= _free_vars(effect) + result |= free_vars(effect) return result case Allocate(): @@ -97,9 +97,9 @@ def _free_vars(term: Term) -> set[Identifier]: case Let(bindings=bindings, body=body): names = [name for name, _ in bindings] - result = _free_vars(body) - set(names) + result = free_vars(body) - set(names) for _, value in bindings: - result |= _free_vars(value) + result |= free_vars(value) return result @@ -114,14 +114,14 @@ def dead_code_elimination_term( new_values = [(name, recur(value)) for name, value in bindings] new_body = recur(body) - live = _free_vars(new_body) + live = free_vars(new_body) kept_reversed: list[tuple[Identifier, Term]] = [] for name, value in reversed(new_values): - if name in live or not _is_pure(value): + if name in live or not is_pure(value): kept_reversed.append((name, value)) live.discard(name) - live |= _free_vars(value) + live |= free_vars(value) kept = list(reversed(kept_reversed)) if len(kept) == 0: @@ -171,7 +171,7 @@ def dead_code_elimination_term( case Begin(effects=effects, value=value): # pragma: no branch new_effects = [recur(effect) for effect in effects] - kept_effects = [effect for effect in new_effects if not _is_pure(effect)] + kept_effects = [effect for effect in new_effects if not is_pure(effect)] new_value = recur(value) if len(kept_effects) == 0: From 45e443426a52e46577a140bd61720658afc7550c Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Sun, 15 Mar 2026 18:49:48 -0400 Subject: [PATCH 30/35] omtimize tests --- packages/L2/test/L2/test_optimize.py | 579 +++++++++++++++++++++++++-- 1 file changed, 540 insertions(+), 39 deletions(-) diff --git a/packages/L2/test/L2/test_optimize.py b/packages/L2/test/L2/test_optimize.py index 524a6d1..a018941 100644 --- a/packages/L2/test/L2/test_optimize.py +++ b/packages/L2/test/L2/test_optimize.py @@ -1,9 +1,27 @@ -from L2.optimize import optimize_program +from typing import cast + +from L2.constant_folding import constant_folding_term +from L2.constant_propogation import constant_propogation_term +from L2.dead_code_elimination import dead_code_elimination_term, free_vars, is_pure +from L2.optimize import optimize_program, optimize_program_step from L2.syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, Immediate, + Let, + Load, Primitive, Program, + Reference, + Store, + Term, ) +from pydantic import ValidationError + +# Used copilot to help with writing these tests, gave it my expected input and output and it generated tests that I then modified to be more comprehensive and cover more edge cases. def test_optimize_program(): @@ -26,67 +44,550 @@ def test_optimize_program(): assert actual == expected -def test_optimize_program_no_optimization(): - program = Program( - parameters=[], - body=Primitive( +def test_constant_propogation_term_all_cases(): + term = Let( + bindings=[ + ("x", Immediate(value=1)), + ("y", Reference(name="x")), + ], + body=Begin( + effects=[ + Apply(target=Reference(name="f"), arguments=[Reference(name="y")]), + Store(base=Reference(name="arr"), index=0, value=Reference(name="x")), + ], + value=Branch( + operator="<", + left=Primitive(operator="+", left=Reference(name="x"), right=Immediate(value=2)), + right=Immediate(value=5), + consequent=Load(base=Allocate(count=1), index=0), + otherwise=Abstract(parameters=["x"], body=Reference(name="x")), + ), + ), + ) + + expected = Let( + bindings=[ + ("x", Immediate(value=1)), + ("y", Reference(name="x")), + ], + body=Begin( + effects=[ + Apply(target=Reference(name="f"), arguments=[Reference(name="x")]), + Store(base=Reference(name="arr"), index=0, value=Immediate(value=1)), + ], + value=Branch( + operator="<", + left=Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2)), + right=Immediate(value=5), + consequent=Load(base=Allocate(count=1), index=0), + otherwise=Abstract(parameters=["x"], body=Reference(name="x")), + ), + ), + ) + + actual = constant_propogation_term(term, context={"f": Reference(name="f")}) + + assert actual == expected + + +def test_constant_folding_plus_cases(): + term = Primitive( + operator="+", + left=Primitive(operator="+", left=Immediate(value=2), right=Reference(name="a")), + right=Primitive(operator="+", left=Immediate(value=3), right=Reference(name="b")), + ) + + expected = Primitive( + operator="+", + left=Immediate(value=5), + right=Primitive( operator="+", + left=Reference(name="a"), + right=Reference(name="b"), + ), + ) + + actual = constant_folding_term(term, context={}) + + assert actual == expected + + actual_immediate = constant_folding_term( + Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2)), + context={}, + ) + assert actual_immediate == Immediate(value=3) + + actual_zero = constant_folding_term( + Primitive(operator="+", left=Immediate(value=0), right=Reference(name="x")), + context={}, + ) + assert actual_zero == Reference(name="x") + + actual_normalize = constant_folding_term( + Primitive(operator="+", left=Reference(name="x"), right=Immediate(value=9)), + context={}, + ) + assert actual_normalize == Primitive( + operator="+", + left=Immediate(value=9), + right=Reference(name="x"), + ) + + +def test_constant_folding_minus_and_multiply_cases(): + actual_subtract = constant_folding_term( + Primitive(operator="-", left=Immediate(value=9), right=Immediate(value=4)), + context={}, + ) + assert actual_subtract == Immediate(value=5) + + actual_subtract_not_folded = constant_folding_term( + Primitive(operator="-", left=Reference(name="x"), right=Immediate(value=4)), + context={}, + ) + assert actual_subtract_not_folded == Primitive( + operator="-", + left=Reference(name="x"), + right=Immediate(value=4), + ) + + actual_multiply = constant_folding_term( + Primitive(operator="*", left=Immediate(value=3), right=Immediate(value=4)), + context={}, + ) + assert actual_multiply == Immediate(value=12) + + actual_mul_left_zero = constant_folding_term( + Primitive(operator="*", left=Immediate(value=0), right=Reference(name="x")), + context={}, + ) + assert actual_mul_left_zero == Immediate(value=0) + + actual_mul_right_zero = constant_folding_term( + Primitive(operator="*", left=Reference(name="x"), right=Immediate(value=0)), + context={}, + ) + assert actual_mul_right_zero == Immediate(value=0) + + actual_mul_left_one = constant_folding_term( + Primitive(operator="*", left=Immediate(value=1), right=Reference(name="x")), + context={}, + ) + assert actual_mul_left_one == Reference(name="x") + + actual_mul_right_one = constant_folding_term( + Primitive(operator="*", left=Reference(name="x"), right=Immediate(value=1)), + context={}, + ) + assert actual_mul_right_one == Reference(name="x") + + actual_mul_normalize = constant_folding_term( + Primitive(operator="*", left=Reference(name="x"), right=Immediate(value=7)), + context={}, + ) + assert actual_mul_normalize == Primitive( + operator="*", + left=Immediate(value=7), + right=Reference(name="x"), + ) + + actual_mul_not_folded = constant_folding_term( + Primitive(operator="*", left=Reference(name="x"), right=Reference(name="y")), + context={}, + ) + assert actual_mul_not_folded == Primitive( + operator="*", + left=Reference(name="x"), + right=Reference(name="y"), + ) + + +def test_constant_folding_non_primitive_cases_and_branches(): + actual_reference_hit = constant_folding_term(Reference(name="x"), context={"x": Immediate(value=4)}) + assert actual_reference_hit == Immediate(value=4) + + actual_reference_miss = constant_folding_term(Reference(name="y"), context={"x": Immediate(value=4)}) + assert actual_reference_miss == Reference(name="y") + + actual_abstract = constant_folding_term( + Abstract(parameters=["x"], body=Primitive(operator="+", left=Reference(name="x"), right=Immediate(value=1))), + context={}, + ) + assert actual_abstract == Abstract( + parameters=["x"], + body=Primitive(operator="+", left=Immediate(value=1), right=Reference(name="x")), + ) + + actual_apply = constant_folding_term( + Apply( + target=Reference(name="f"), + arguments=[Primitive(operator="+", left=Immediate(value=2), right=Immediate(value=3))], + ), + context={}, + ) + assert actual_apply == Apply(target=Reference(name="f"), arguments=[Immediate(value=5)]) + + actual_immediate = constant_folding_term(Immediate(value=11), context={}) + assert actual_immediate == Immediate(value=11) + + actual_lt_true = constant_folding_term( + Branch( + operator="<", left=Immediate(value=1), right=Immediate(value=2), + consequent=Immediate(value=7), + otherwise=Immediate(value=8), ), + context={}, ) + assert actual_lt_true == Immediate(value=7) - expected = program + actual_lt_false = constant_folding_term( + Branch( + operator="<", + left=Immediate(value=4), + right=Immediate(value=2), + consequent=Immediate(value=7), + otherwise=Immediate(value=8), + ), + context={}, + ) + assert actual_lt_false == Immediate(value=8) - actual = optimize_program(program) + actual_lt_fallback = constant_folding_term( + Branch( + operator="<", + left=Reference(name="x"), + right=Immediate(value=2), + consequent=Immediate(value=7), + otherwise=Immediate(value=8), + ), + context={}, + ) + assert actual_lt_fallback == Branch( + operator="<", + left=Reference(name="x"), + right=Immediate(value=2), + consequent=Immediate(value=7), + otherwise=Immediate(value=8), + ) - assert actual == expected + actual_eq_true = constant_folding_term( + Branch( + operator="==", + left=Immediate(value=3), + right=Immediate(value=3), + consequent=Immediate(value=9), + otherwise=Immediate(value=10), + ), + context={}, + ) + assert actual_eq_true == Immediate(value=9) + actual_eq_false = constant_folding_term( + Branch( + operator="==", + left=Immediate(value=3), + right=Immediate(value=4), + consequent=Immediate(value=9), + otherwise=Immediate(value=10), + ), + context={}, + ) + assert actual_eq_false == Immediate(value=10) -def test_optimize_program_nested(): - program = Program( - parameters=[], - body=Primitive( - operator="+", - left=Primitive( - operator="+", + actual_eq_fallback = constant_folding_term( + Branch( + operator="==", + left=Reference(name="x"), + right=Immediate(value=4), + consequent=Immediate(value=9), + otherwise=Immediate(value=10), + ), + context={}, + ) + assert actual_eq_fallback == Branch( + operator="==", + left=Reference(name="x"), + right=Immediate(value=4), + consequent=Immediate(value=9), + otherwise=Immediate(value=10), + ) + + actual_allocate = constant_folding_term(Allocate(count=3), context={}) + assert actual_allocate == Allocate(count=3) + + actual_load = constant_folding_term( + Load(base=Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2)), index=0), context={} + ) + assert actual_load == Load(base=Immediate(value=3), index=0) + + actual_store = constant_folding_term( + Store( + base=Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2)), + index=0, + value=Primitive(operator="+", left=Immediate(value=3), right=Immediate(value=4)), + ), + context={}, + ) + assert actual_store == Store(base=Immediate(value=3), index=0, value=Immediate(value=7)) + + actual_begin = constant_folding_term( + Begin( + effects=[Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2))], + value=Primitive(operator="+", left=Immediate(value=3), right=Immediate(value=4)), + ), + context={}, + ) + assert actual_begin == Begin(effects=[Immediate(value=3)], value=Immediate(value=7)) + + +def test_dead_codeis_pure_andfree_vars_cases(): + assert is_pure(Immediate(value=1)) is True + assert is_pure(Reference(name="x")) is True + assert is_pure(Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2))) is True + assert is_pure(Abstract(parameters=["x"], body=Reference(name="x"))) is True + assert is_pure(Let(bindings=[("x", Immediate(value=1))], body=Reference(name="x"))) is True + assert ( + is_pure( + Branch( + operator="<", left=Immediate(value=1), - right=Immediate(value=1), - ), - right=Immediate(value=1), + right=Immediate(value=2), + consequent=Immediate(value=3), + otherwise=Immediate(value=4), + ) + ) + is True + ) + assert is_pure(Load(base=Reference(name="x"), index=0)) is True + assert is_pure(Begin(effects=[Immediate(value=1)], value=Reference(name="x"))) is True + assert is_pure(Apply(target=Reference(name="f"), arguments=[])) is False + assert is_pure(Allocate(count=1)) is False + assert is_pure(Store(base=Reference(name="x"), index=0, value=Immediate(value=1))) is False + + assert free_vars(Immediate(value=1)) == set() + assert free_vars(Reference(name="x")) == {"x"} + assert free_vars(Primitive(operator="+", left=Reference(name="x"), right=Reference(name="y"))) == {"x", "y"} + assert free_vars(Apply(target=Reference(name="f"), arguments=[Reference(name="x")])) == {"f", "x"} + assert free_vars( + Abstract(parameters=["x"], body=Primitive(operator="+", left=Reference(name="x"), right=Reference(name="y"))) + ) == {"y"} + assert free_vars( + Branch( + operator="<", + left=Reference(name="a"), + right=Reference(name="b"), + consequent=Reference(name="c"), + otherwise=Reference(name="d"), + ) + ) == {"a", "b", "c", "d"} + assert free_vars(Load(base=Reference(name="arr"), index=0)) == {"arr"} + assert free_vars(Store(base=Reference(name="arr"), index=0, value=Reference(name="v"))) == {"arr", "v"} + assert free_vars(Begin(effects=[Reference(name="u")], value=Reference(name="v"))) == {"u", "v"} + assert free_vars(Allocate(count=1)) == set() + assert free_vars( + Let( + bindings=[ + ("x", Reference(name="a")), + ("y", Primitive(operator="+", left=Reference(name="x"), right=Reference(name="b"))), + ], + body=Primitive(operator="+", left=Reference(name="y"), right=Reference(name="c")), + ) + ) == {"a", "b", "c", "x"} + + +def test_dead_code_elimination_term_all_cases(): + term_drop_let = Let( + bindings=[("x", Immediate(value=1))], + body=Immediate(value=7), + ) + expected_drop_let = Immediate(value=7) + actual_drop_let = dead_code_elimination_term(term_drop_let, context={}) + assert actual_drop_let == expected_drop_let + + term_keep_let = Let( + bindings=[("x", Store(base=Reference(name="arr"), index=0, value=Immediate(value=1)))], + body=Immediate(value=7), + ) + expected_keep_let = Let( + bindings=[("x", Store(base=Reference(name="arr"), index=0, value=Immediate(value=1)))], + body=Immediate(value=7), + ) + actual_keep_let = dead_code_elimination_term(term_keep_let, context={}) + assert actual_keep_let == expected_keep_let + + actual_reference = dead_code_elimination_term(Reference(name="x"), context={}) + assert actual_reference == Reference(name="x") + + actual_abstract = dead_code_elimination_term( + Abstract(parameters=["x"], body=Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2))), + context={}, + ) + assert actual_abstract == Abstract( + parameters=["x"], body=Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2)) + ) + + actual_apply = dead_code_elimination_term( + Apply( + target=Reference(name="f"), + arguments=[Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2))], ), + context={}, + ) + assert actual_apply == Apply( + target=Reference(name="f"), + arguments=[Primitive(operator="+", left=Immediate(value=1), right=Immediate(value=2))], ) - expected = Program( - parameters=[], - body=Primitive( - operator="+", - left=Immediate(value=2), - right=Immediate(value=1), + actual_immediate = dead_code_elimination_term(Immediate(value=5), context={}) + assert actual_immediate == Immediate(value=5) + + actual_primitive = dead_code_elimination_term( + Primitive(operator="+", left=Reference(name="x"), right=Immediate(value=2)), + context={}, + ) + assert actual_primitive == Primitive(operator="+", left=Reference(name="x"), right=Immediate(value=2)) + + actual_branch = dead_code_elimination_term( + Branch( + operator="==", + left=Reference(name="x"), + right=Immediate(value=2), + consequent=Immediate(value=1), + otherwise=Immediate(value=0), ), + context={}, + ) + assert actual_branch == Branch( + operator="==", + left=Reference(name="x"), + right=Immediate(value=2), + consequent=Immediate(value=1), + otherwise=Immediate(value=0), ) - actual = optimize_program(program) + actual_allocate = dead_code_elimination_term(Allocate(count=2), context={}) + assert actual_allocate == Allocate(count=2) - assert actual == expected + actual_load = dead_code_elimination_term(Load(base=Reference(name="arr"), index=0), context={}) + assert actual_load == Load(base=Reference(name="arr"), index=0) + actual_store = dead_code_elimination_term( + Store(base=Reference(name="arr"), index=0, value=Immediate(value=7)), + context={}, + ) + assert actual_store == Store(base=Reference(name="arr"), index=0, value=Immediate(value=7)) -def test_optimize_program_no_optimization_nested(): - program = Program( + term_begin_drop = Begin(effects=[Immediate(value=1)], value=Reference(name="x")) + expected_begin_drop = Reference(name="x") + actual_begin_drop = dead_code_elimination_term(term_begin_drop, context={}) + assert actual_begin_drop == expected_begin_drop + + term_begin_keep = Begin( + effects=[Immediate(value=1), Store(base=Reference(name="arr"), index=0, value=Immediate(value=9))], + value=Reference(name="x"), + ) + expected_begin_keep = Begin( + effects=[Store(base=Reference(name="arr"), index=0, value=Immediate(value=9))], + value=Reference(name="x"), + ) + actual_begin_keep = dead_code_elimination_term(term_begin_keep, context={}) + assert actual_begin_keep == expected_begin_keep + + +def test_optimize_program_step_and_optimize_program(): + program_change = Program( parameters=[], - body=Primitive( - operator="+", - left=Primitive( - operator="+", - left=Immediate(value=1), - right=Immediate(value=2), - ), - right=Immediate(value=1), + body=Let( + bindings=[ + ("x", Immediate(value=1)), + ("y", Reference(name="x")), + ("z", Primitive(operator="+", left=Reference(name="y"), right=Immediate(value=2))), + ], + body=Reference(name="z"), ), ) - expected = program + expected_step_program = Program( + parameters=[], + body=Let( + bindings=[ + ("x", Immediate(value=1)), + ("y", Reference(name="x")), + ("z", Primitive(operator="+", left=Immediate(value=2), right=Reference(name="y"))), + ], + body=Reference(name="z"), + ), + ) - actual = optimize_program(program) + actual_step_program, changed = optimize_program_step(program_change) - assert actual == expected + assert actual_step_program == expected_step_program + assert changed is True + + program_no_change = Program( + parameters=["x"], + body=Reference(name="x"), + ) + + actual_same_program, changed_same = optimize_program_step(program_no_change) + + assert actual_same_program == program_no_change + assert changed_same is False + + actual_optimize = optimize_program(program_change) + + assert actual_optimize == expected_step_program + + actual_optimize_no_change = optimize_program(program_no_change) + + assert actual_optimize_no_change == program_no_change + + +def test_dead_code_helper_fallthrough_cases(): + invalid_term = cast(Term, object()) + + actualis_pure = is_pure(invalid_term) + assert actualis_pure is None + + actualfree_vars = free_vars(invalid_term) + assert actualfree_vars is None + + +def test_constant_folding_edge_fallthrough_cases(): + plus_invalid = Primitive.model_construct( + operator="+", + left=Reference(name="x"), + right=object(), + ) + with_validation_error_plus = False + try: + constant_folding_term(plus_invalid, context={}) + except ValidationError: + with_validation_error_plus = True + assert with_validation_error_plus is True + + minus_invalid = Primitive.model_construct( + operator="-", + left=Reference(name="x"), + right=object(), + ) + with_validation_error_minus = False + try: + constant_folding_term(minus_invalid, context={}) + except ValidationError: + with_validation_error_minus = True + assert with_validation_error_minus is True + + multiply_invalid = Primitive.model_construct( + operator="*", + left=Reference(name="x"), + right=object(), + ) + with_validation_error_multiply = False + try: + constant_folding_term(multiply_invalid, context={}) + except ValidationError: + with_validation_error_multiply = True + assert with_validation_error_multiply is True From ce5cedba153fc941a99905a12e68727ec942d443 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Mon, 16 Mar 2026 13:44:08 -0400 Subject: [PATCH 31/35] Had to fix some eliminate letrec errors... --- packages/L3/src/L3/eliminate_letrec.py | 98 ++++++++++++++++++++------ 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/packages/L3/src/L3/eliminate_letrec.py b/packages/L3/src/L3/eliminate_letrec.py index 4f668cf..cc6b044 100644 --- a/packages/L3/src/L3/eliminate_letrec.py +++ b/packages/L3/src/L3/eliminate_letrec.py @@ -1,6 +1,5 @@ # noqa: F841 from collections.abc import Mapping -from functools import partial from L2 import syntax as L2 @@ -13,20 +12,73 @@ def eliminate_letrec_term( term: L3.Term, context: Context, ) -> L2.Term: - recur = partial(eliminate_letrec_term, context=context) - match term: case L3.Let(bindings=bindings, body=body): return L2.Let( - bindings=[(name, recur(value)) for name, value in bindings], - body=recur(body), + bindings=[(name, eliminate_letrec_term(value, context)) for name, value in bindings], + body=eliminate_letrec_term(body, context), ) case L3.LetRec(bindings=bindings, body=body): - return L2.Let( - bindings=[(name, recur(value)) for name, value in bindings], - body=recur(body, context={**context, **dict.fromkeys([name for name, _ in bindings])}), - ) + # Mark all binding names as recursive in the context + binding_names = [name for name, _ in bindings] + new_context: Context = {**context, **dict.fromkeys(binding_names)} # type: ignore + + # Check which bindings need heap allocation based on their values + # Simple values (Immediate, Allocate) can be stored directly + # Complex values (everything else) need Allocate + Store + simple_binding_indices: set[int] = set() + for i, (_, value) in enumerate(bindings): + match value: + case L3.Immediate() | L3.Allocate(): + simple_binding_indices.add(i) + case _: + pass + + # Separate simple and complex bindings + simple_bindings: list[tuple[str, L2.Term]] = [] + complex_bindings: list[tuple[str, L3.Term]] = [] + complex_binding_names: list[str] = [] + + for i, (name, value) in enumerate(bindings): + if i in simple_binding_indices: + transformed_value = eliminate_letrec_term(value, new_context) + simple_bindings.append((name, transformed_value)) + else: + complex_bindings.append((name, value)) + complex_binding_names.append(name) + + # Create stores for complex bindings + stores: list[L2.Term] = [] + for name, value in complex_bindings: + transformed_value = eliminate_letrec_term(value, new_context) + stores.append( + L2.Store( + base=L2.Reference(name=name), + index=0, + value=transformed_value, + ) + ) + + # Transform the body + transformed_body = eliminate_letrec_term(body, new_context) + + # Build the result + all_bindings = simple_bindings + [(name, L2.Allocate(count=1)) for name in complex_binding_names] + + if stores: + return L2.Let( + bindings=all_bindings, + body=L2.Begin( + effects=stores, + value=transformed_body, + ), + ) + else: + return L2.Let( + bindings=all_bindings, + body=transformed_body, + ) case L3.Reference(name=name): # if name is a recursive variable -> (Load (Reference name))) @@ -37,12 +89,12 @@ def eliminate_letrec_term( return L2.Reference(name=name) case L3.Abstract(parameters=parameters, body=body): - return L2.Abstract(parameters=parameters, body=recur(body)) + return L2.Abstract(parameters=parameters, body=eliminate_letrec_term(body, context)) case L3.Apply(target=target, arguments=arguments): return L2.Apply( - target=recur(target), - arguments=[recur(argument) for argument in arguments], + target=eliminate_letrec_term(target, context), + arguments=[eliminate_letrec_term(argument, context) for argument in arguments], ) case L3.Immediate(value=value): @@ -51,17 +103,17 @@ def eliminate_letrec_term( case L3.Primitive(operator=operator, left=left, right=right): return L2.Primitive( operator=operator, - left=recur(left), - right=recur(right), + left=eliminate_letrec_term(left, context), + right=eliminate_letrec_term(right, context), ) case L3.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): return L2.Branch( operator=operator, - left=recur(left), - right=recur(right), - consequent=recur(consequent), - otherwise=recur(otherwise), + left=eliminate_letrec_term(left, context), + right=eliminate_letrec_term(right, context), + consequent=eliminate_letrec_term(consequent, context), + otherwise=eliminate_letrec_term(otherwise, context), ) case L3.Allocate(count=count): @@ -69,21 +121,21 @@ def eliminate_letrec_term( case L3.Load(base=base, index=index): return L2.Load( - base=recur(base), + base=eliminate_letrec_term(base, context), index=index, ) case L3.Store(base=base, index=index, value=value): return L2.Store( - base=recur(base), + base=eliminate_letrec_term(base, context), index=index, - value=recur(value), + value=eliminate_letrec_term(value, context), ) case L3.Begin(effects=effects, value=value): # pragma: no branch return L2.Begin( - effects=[recur(effect) for effect in effects], - value=recur(value), + effects=[eliminate_letrec_term(effect, context) for effect in effects], + value=eliminate_letrec_term(value, context), ) From b3494b8e2464635e12e14100967417305d50d2fd Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 20 Mar 2026 15:45:49 -0400 Subject: [PATCH 32/35] Finished unify and tests --- packages/L3/src/L3/uniqify.py | 10 +- packages/L3/test/L3/test_uniqify.py | 146 +++++++++++++++++++++++++++- 2 files changed, 150 insertions(+), 6 deletions(-) diff --git a/packages/L3/src/L3/uniqify.py b/packages/L3/src/L3/uniqify.py index d553499..df1dec0 100644 --- a/packages/L3/src/L3/uniqify.py +++ b/packages/L3/src/L3/uniqify.py @@ -42,7 +42,7 @@ def uniqify_term( return Let( bindings=new_bindings, - body=_term(body, new_context), + body=uniqify_term(body, new_context, fresh), ) case LetRec(bindings=bindings, body=body): @@ -53,9 +53,11 @@ def uniqify_term( new_bindings.append((new_name, value)) new_context = {**new_context, name: new_name} + _new_term = partial(uniqify_term, context=new_context, fresh=fresh) + return LetRec( - bindings=[(new_name, _term(value)) for new_name, value in new_bindings], - body=_term(body, new_context), + bindings=[(new_name, _new_term(value)) for new_name, value in new_bindings], + body=_new_term(body), ) case Reference(name=name): @@ -71,7 +73,7 @@ def uniqify_term( } return Abstract( parameters=new_parameters, - body=_term(body, new_context), + body=uniqify_term(body, new_context, fresh), ) case Apply(target=target, arguments=arguments): diff --git a/packages/L3/test/L3/test_uniqify.py b/packages/L3/test/L3/test_uniqify.py index e2243e9..3fdaf62 100644 --- a/packages/L3/test/L3/test_uniqify.py +++ b/packages/L3/test/L3/test_uniqify.py @@ -1,5 +1,19 @@ -from L3.syntax import Apply, Immediate, Let, Reference -from L3.uniqify import Context, uniqify_term +from L3.syntax import ( + Abstract, + Allocate, + Apply, + Begin, + Branch, + Immediate, + Let, + LetRec, + Load, + Primitive, + Program, + Reference, + Store, +) +from L3.uniqify import Context, uniqify_program, uniqify_term from util.sequential_name_generator import SequentialNameGenerator @@ -15,6 +29,18 @@ def test_uniqify_term_reference(): assert actual == expected +def test_uniqify_term_reference_not_in_context(): + term = Reference(name="x") + + context: Context = {} + fresh = SequentialNameGenerator() + actual = uniqify_term(term, context, fresh=fresh) + + expected = Reference(name="x") + + assert actual == expected + + def test_uniqify_immediate(): term = Immediate(value=42) @@ -59,3 +85,119 @@ def test_uniqify_term_let(): ) assert actual == expected + + +def test_uniqify_term_letrec_and_abstract_and_apply(): + term = LetRec( + bindings=[ + ( + "f", + Abstract( + parameters=["x"], + body=Apply( + target=Reference(name="f"), + arguments=[Reference(name="x")], + ), + ), + ), + ("y", Reference(name="f")), + ], + body=Reference(name="y"), + ) + + context: Context = {"f": "outer_f"} + fresh = SequentialNameGenerator() + actual = uniqify_term(term, context, fresh) + + expected = LetRec( + bindings=[ + ( + "f0", + Abstract( + parameters=["x0"], + body=Apply( + target=Reference(name="f0"), + arguments=[Reference(name="x0")], + ), + ), + ), + ("y0", Reference(name="f0")), + ], + body=Reference(name="y0"), + ) + + assert actual == expected + + +def test_uniqify_term_memory_and_control_forms(): + term = Begin( + effects=[ + Store( + base=Reference(name="ptr"), + index=0, + value=Primitive( + operator="+", + left=Reference(name="a"), + right=Reference(name="b"), + ), + ) + ], + value=Branch( + operator="<", + left=Reference(name="a"), + right=Immediate(value=0), + consequent=Allocate(count=1), + otherwise=Load(base=Reference(name="ptr"), index=1), + ), + ) + + context: Context = {"a": "a1", "b": "b1", "ptr": "p1"} + fresh = SequentialNameGenerator() + actual = uniqify_term(term, context, fresh) + + expected = Begin( + effects=[ + Store( + base=Reference(name="p1"), + index=0, + value=Primitive( + operator="+", + left=Reference(name="a1"), + right=Reference(name="b1"), + ), + ) + ], + value=Branch( + operator="<", + left=Reference(name="a1"), + right=Immediate(value=0), + consequent=Allocate(count=1), + otherwise=Load(base=Reference(name="p1"), index=1), + ), + ) + + assert actual == expected + + +def test_uniqify_program(): + program = Program( + parameters=["x", "y"], + body=Primitive( + operator="+", + left=Reference(name="x"), + right=Reference(name="y"), + ), + ) + + _, actual = uniqify_program(program) + + expected = Program( + parameters=["x0", "y0"], + body=Primitive( + operator="+", + left=Reference(name="x0"), + right=Reference(name="y0"), + ), + ) + + assert actual == expected From 26f20e6ca15ecd6bafdfeff58dd1bf0f16e343ce Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 20 Mar 2026 15:58:34 -0400 Subject: [PATCH 33/35] Added a comment to push a new change for codecov to hopefully run --- packages/L3/src/L3/uniqify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/L3/src/L3/uniqify.py b/packages/L3/src/L3/uniqify.py index df1dec0..ba3180a 100644 --- a/packages/L3/src/L3/uniqify.py +++ b/packages/L3/src/L3/uniqify.py @@ -24,6 +24,7 @@ type Context = Mapping[Identifier, Identifier] +# This pass is responsible for renaming all identifiers in a program to be unique. This is necessary for the later stages of the compiler, which rely on the fact that all identifiers are unique. def uniqify_term( term: Term, context: Context, From 88a274fef10a0b5c1b199ad06658a660ed37ab8c Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 3 Apr 2026 13:55:39 -0400 Subject: [PATCH 34/35] Current class changes to cps --- packages/L2/src/L2/cps_convert.py | 230 +++++++++++++++++++----------- 1 file changed, 148 insertions(+), 82 deletions(-) diff --git a/packages/L2/src/L2/cps_convert.py b/packages/L2/src/L2/cps_convert.py index 62eddb8..953f293 100644 --- a/packages/L2/src/L2/cps_convert.py +++ b/packages/L2/src/L2/cps_convert.py @@ -1,82 +1,148 @@ -# from collections.abc import Callable, Sequence -# from functools import partial - -# from L1 import syntax as L1 - -# from L2 import syntax as L2 - - -# def cps_convert_term( -# term: L2.Term, -# k: Callable[[L1.Identifier], L1.Statement], -# fresh: Callable[[str], str], -# ) -> L1.Statement: -# _term = partial(cps_convert_term, fresh=fresh) -# _terms = partial(cps_convert_terms, fresh=fresh) - -# match term: -# case L2.Let(bindings=bindings, body=body): -# pass - -# case L2.Reference(name=name): -# pass - -# case L2.Abstract(parameters=parameters, body=body): -# pass - -# case L2.Apply(target=target, arguments=arguments): -# pass - -# case L2.Immediate(value=value): -# pass - -# case L2.Primitive(operator=operator, left=left, right=right): -# pass - -# case L2.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): -# pass - -# case L2.Allocate(count=count): -# pass - -# case L2.Load(base=base, index=index): -# pass - -# case L2.Store(base=base, index=index, value=value): -# pass - -# case L2.Begin(effects=effects, value=value): # pragma: no branch -# pass - - -# def cps_convert_terms( -# terms: Sequence[L2.Term], -# k: Callable[[Sequence[L1.Identifier]], L1.Statement], -# fresh: Callable[[str], str], -# ) -> L1.Statement: -# _term = partial(cps_convert_term, fresh=fresh) -# _terms = partial(cps_convert_terms, fresh=fresh) - -# match terms: -# case []: -# return k([]) - -# case [first, *rest]: -# return _term(first, lambda first: _terms(rest, lambda rest: k([first, *rest]))) - -# case _: # pragma: no cover -# raise ValueError(terms) - - -# def cps_convert_program( -# program: L2.Program, -# fresh: Callable[[str], str], -# ) -> L1.Program: -# _term = partial(cps_convert_term, fresh=fresh) - -# match program: -# case L2.Program(parameters=parameters, body=body): # pragma: no branch -# return L1.Program( -# parameters=parameters, -# body=_term(body, lambda value: L1.Halt(value=value)), -# ) +from collections.abc import Callable, Sequence +from functools import partial + +from L1 import syntax as L1 + +from L2 import syntax as L2 + + +def cps_convert_term( + term: L2.Term, + m: Callable[[L1.Identifier], L1.Statement], + fresh: Callable[[str], str], +) -> L1.Statement: + _term = partial(cps_convert_term, fresh=fresh) + _terms = partial(cps_convert_terms, fresh=fresh) + + match term: + case L2.Let(bindings=bindings, body=body): + result = _term(body, m) + + for name, value in reversed(bindings): + result = _term(value, lambda value: L1.Copy(destination=name, source=value, then=result)) + + return result + + case L2.Reference(name=name): + return m(name) + + case L2.Abstract(parameters=parameters, body=body): + tmp = fresh("t") + k = fresh("k") + L1.Abstract( + destination=tmp, + parameters=[*parameters, k], + body=_term(body, lambda body: L1.Apply(target=k, arguments=[body])), + then=m(tmp), + ) + + case L2.Apply(target=target, arguments=arguments): + k = fresh("k") + tmp = fresh("t") + return L1.Abstract( + destination=k, + parameters=[tmp], + body=m(tmp), + then=_term( + target, + lambda target: _terms( + arguments, + lambda arguments: L1.Apply(target=target, arguments=[*arguments, k]), + ), + ), + ) + + case L2.Immediate(value=value): + tmp = fresh("t") + return L1.Immediate(destination=tmp, value=value, then=m(tmp)) + + case L2.Primitive(operator=operator, left=left, right=right): + tmp = fresh("t") + return _term( + left, + lambda left: _term( + right, + lambda right: L1.Primitive(destination=tmp, operator=operator, left=left, right=right, then=m(tmp)), + ), + ) + + case L2.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): + return _term( + left, + lambda left: _term( + right, + lambda right: L1.Branch( + operator=operator, + left=left, + right=right, + then=_term(consequent, lambda consequent: L1.Apply()), + otherwise=_term(otherwise, m), + ), + ), + ) + + case L2.Allocate(count=count): + tmp = fresh("t") + return L1.Allocate(destination=tmp, count=count, then=m(tmp)) + + case L2.Load(base=base, index=index): + tmp = fresh("t") + return _term( + base, + lambda base: L1.Load(destination=tmp, base=base, index=index, then=m(tmp)), + ) + + # Should double check this + case L2.Store(base=base, index=index, value=value): + tmp = fresh("t") + return _term( + base, + lambda base: _term( + value, + lambda value: L1.Store( + base=base, index=index, value=value, then=L1.Immediate(destination=tmp, value=0, then=m(tmp)) + ), + ), + ) + + case L2.Begin(effects=effects, value=value): # pragma: no branch + return _terms( + effects, + lambda effects: _term( + value, + lambda value: m(value), + ), + ) + + +def cps_convert_terms( + terms: Sequence[L2.Term], + k: Callable[[Sequence[L1.Identifier]], L1.Statement], + fresh: Callable[[str], str], +) -> L1.Statement: + _term = partial(cps_convert_term, fresh=fresh) + _terms = partial(cps_convert_terms, fresh=fresh) + + match terms: + case []: + return k([]) + + case [first, *rest]: + return _term(first, lambda first: _terms(rest, lambda rest: k([first, *rest]))) + + case _: # pragma: no cover + raise ValueError(terms) + + +def cps_convert_program( + program: L2.Program, + fresh: Callable[[str], str], +) -> L1.Program: + _term = partial(cps_convert_term, fresh=fresh) + + match program: + case L2.Program(parameters=parameters, body=body): # pragma: no branch + return L1.Program( + parameters=parameters, + body=_term(body, lambda value: L1.Halt(value=value)), + ) From 0064c43a723481b9d725999e6a0f313d212476c7 Mon Sep 17 00:00:00 2001 From: John Fulkerson Date: Fri, 3 Apr 2026 14:15:10 -0400 Subject: [PATCH 35/35] Uncommented tests and finished cps convert --- packages/L2/src/L2/cps_convert.py | 29 +- packages/L2/test/L2/test_cps_convert.py | 444 ++++++++++++------------ 2 files changed, 240 insertions(+), 233 deletions(-) diff --git a/packages/L2/src/L2/cps_convert.py b/packages/L2/src/L2/cps_convert.py index 953f293..b9f1b2a 100644 --- a/packages/L2/src/L2/cps_convert.py +++ b/packages/L2/src/L2/cps_convert.py @@ -29,7 +29,7 @@ def cps_convert_term( case L2.Abstract(parameters=parameters, body=body): tmp = fresh("t") k = fresh("k") - L1.Abstract( + return L1.Abstract( destination=tmp, parameters=[*parameters, k], body=_term(body, lambda body: L1.Apply(target=k, arguments=[body])), @@ -67,16 +67,23 @@ def cps_convert_term( ) case L2.Branch(operator=operator, left=left, right=right, consequent=consequent, otherwise=otherwise): - return _term( - left, - lambda left: _term( - right, - lambda right: L1.Branch( - operator=operator, - left=left, - right=right, - then=_term(consequent, lambda consequent: L1.Apply()), - otherwise=_term(otherwise, m), + j = fresh("j") + tmp = fresh("t") + return L1.Abstract( + destination=j, + parameters=[tmp], + body=m(tmp), + then=_term( + left, + lambda left: _term( + right, + lambda right: L1.Branch( + operator=operator, + left=left, + right=right, + then=_term(consequent, lambda consequent: L1.Apply(target=j, arguments=[consequent])), + otherwise=_term(otherwise, lambda otherwise: L1.Apply(target=j, arguments=[otherwise])), + ), ), ), ) diff --git a/packages/L2/test/L2/test_cps_convert.py b/packages/L2/test/L2/test_cps_convert.py index cf45d29..1d47e2a 100644 --- a/packages/L2/test/L2/test_cps_convert.py +++ b/packages/L2/test/L2/test_cps_convert.py @@ -1,247 +1,247 @@ -# from L1 import syntax as L1 -# from L2 import syntax as L2 -# from L2.cps_convert import cps_convert_program, cps_convert_term -# from util.sequential_name_generator import SequentialNameGenerator +from L1 import syntax as L1 +from L2 import syntax as L2 +from L2.cps_convert import cps_convert_program, cps_convert_term +from util.sequential_name_generator import SequentialNameGenerator -# def k(v: L1.Identifier) -> L1.Statement: -# return L1.Halt(value=v) +def k(v: L1.Identifier) -> L1.Statement: + return L1.Halt(value=v) -# def test_cps_convert_term_name(): -# term = L2.Reference(name="x") +def test_cps_convert_term_name(): + term = L2.Reference(name="x") -# fresh = SequentialNameGenerator() + fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) + actual = cps_convert_term(term, k, fresh) -# expected = L1.Halt(value="x") -# assert actual == expected + expected = L1.Halt(value="x") + assert actual == expected -# def test_cps_convert_term_immediate(): -# term = L2.Immediate(value=42) +def test_cps_convert_term_immediate(): + term = L2.Immediate(value=42) -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) -# expected = L1.Immediate( -# destination="t0", -# value=42, -# then=L1.Halt(value="t0"), -# ) + expected = L1.Immediate( + destination="t0", + value=42, + then=L1.Halt(value="t0"), + ) -# assert actual == expected + assert actual == expected -# def test_cps_convert_term_primitive(): -# term = L2.Primitive( -# operator="+", -# left=L2.Reference(name="x"), -# right=L2.Reference(name="y"), -# ) +def test_cps_convert_term_primitive(): + term = L2.Primitive( + operator="+", + left=L2.Reference(name="x"), + right=L2.Reference(name="y"), + ) -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) -# expected = L1.Primitive( -# destination="t0", -# operator="+", -# left="x", -# right="y", -# then=L1.Halt(value="t0"), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_let(): -# term = L2.Let( -# bindings=[ -# ("a", L2.Reference(name="x")), -# ("b", L2.Reference(name="y")), -# ], -# body=L2.Reference(name="b"), -# ) + expected = L1.Primitive( + destination="t0", + operator="+", + left="x", + right="y", + then=L1.Halt(value="t0"), + ) + + assert actual == expected + + +def test_cps_convert_term_let(): + term = L2.Let( + bindings=[ + ("a", L2.Reference(name="x")), + ("b", L2.Reference(name="y")), + ], + body=L2.Reference(name="b"), + ) -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Copy( -# destination="a", -# source="x", -# then=L1.Copy( -# destination="b", -# source="y", -# then=L1.Halt(value="b"), -# ), -# ) + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Copy( + destination="a", + source="x", + then=L1.Copy( + destination="b", + source="y", + then=L1.Halt(value="b"), + ), + ) -# assert actual == expected + assert actual == expected -# def test_cps_convert_term_abstract(): -# term = L2.Abstract( -# parameters=["x"], -# body=L2.Reference(name="x"), -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) +def test_cps_convert_term_abstract(): + term = L2.Abstract( + parameters=["x"], + body=L2.Reference(name="x"), + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) -# expected = L1.Abstract( -# destination="t0", -# parameters=["x", "k0"], -# body=L1.Apply(target="k0", arguments=["x"]), -# then=L1.Halt(value="t0"), -# ) + expected = L1.Abstract( + destination="t0", + parameters=["x", "k0"], + body=L1.Apply(target="k0", arguments=["x"]), + then=L1.Halt(value="t0"), + ) -# assert actual == expected + assert actual == expected -# def test_cps_convert_term_apply(): -# term = L2.Apply( -# target=L2.Reference(name="f"), -# arguments=[ -# L2.Reference(name="y"), -# ], -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Abstract( -# destination="k0", -# parameters=["t0"], -# body=L1.Halt(value="t0"), -# then=L1.Apply( -# target="f", -# arguments=["y", "k0"], -# ), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_branch(): -# term = L2.Branch( -# operator="==", -# left=L2.Reference(name="x"), -# right=L2.Reference(name="y"), -# consequent=L2.Reference(name="a"), -# otherwise=L2.Reference(name="b"), -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Abstract( -# destination="j0", -# parameters=["t0"], -# body=L1.Halt(value="t0"), -# then=L1.Branch( -# operator="==", -# left="x", -# right="y", -# then=L1.Apply( -# target="j0", -# arguments=["a"], -# ), -# otherwise=L1.Apply( -# target="j0", -# arguments=["b"], -# ), -# ), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_allocate(): -# term = L2.Allocate(count=0) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Allocate( -# destination="t0", -# count=0, -# then=L1.Halt(value="t0"), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_load(): -# term_load = L2.Load( -# base=L2.Reference(name="x"), -# index=0, -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term_load, k, fresh) - -# expected = L1.Load( -# destination="t0", -# base="x", -# index=0, -# then=L1.Halt(value="t0"), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_store(): -# term = L2.Store( -# base=L2.Reference(name="x"), -# index=0, -# value=L2.Reference(name="y"), -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Store( -# base="x", -# index=0, -# value="y", -# then=L1.Immediate( -# destination="t0", -# value=0, -# then=L1.Halt(value="t0"), -# ), -# ) - -# assert actual == expected - - -# def test_cps_convert_term_begin(): -# term = L2.Begin( -# effects=[ -# L2.Reference(name="x"), -# ], -# value=L2.Reference(name="y"), -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_term(term, k, fresh) - -# expected = L1.Halt(value="y") -# assert actual == expected - - -# def test_cps_convert_program(): -# program = L2.Program( -# parameters=["x"], -# body=L2.Reference(name="x"), -# ) - -# fresh = SequentialNameGenerator() -# actual = cps_convert_program(program, fresh) - -# expected = L1.Program( -# parameters=["x"], -# body=L1.Halt(value="x"), -# ) +def test_cps_convert_term_apply(): + term = L2.Apply( + target=L2.Reference(name="f"), + arguments=[ + L2.Reference(name="y"), + ], + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Abstract( + destination="k0", + parameters=["t0"], + body=L1.Halt(value="t0"), + then=L1.Apply( + target="f", + arguments=["y", "k0"], + ), + ) + + assert actual == expected + + +def test_cps_convert_term_branch(): + term = L2.Branch( + operator="==", + left=L2.Reference(name="x"), + right=L2.Reference(name="y"), + consequent=L2.Reference(name="a"), + otherwise=L2.Reference(name="b"), + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Abstract( + destination="j0", + parameters=["t0"], + body=L1.Halt(value="t0"), + then=L1.Branch( + operator="==", + left="x", + right="y", + then=L1.Apply( + target="j0", + arguments=["a"], + ), + otherwise=L1.Apply( + target="j0", + arguments=["b"], + ), + ), + ) + + assert actual == expected + + +def test_cps_convert_term_allocate(): + term = L2.Allocate(count=0) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Allocate( + destination="t0", + count=0, + then=L1.Halt(value="t0"), + ) + + assert actual == expected + + +def test_cps_convert_term_load(): + term_load = L2.Load( + base=L2.Reference(name="x"), + index=0, + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term_load, k, fresh) + + expected = L1.Load( + destination="t0", + base="x", + index=0, + then=L1.Halt(value="t0"), + ) + + assert actual == expected + + +def test_cps_convert_term_store(): + term = L2.Store( + base=L2.Reference(name="x"), + index=0, + value=L2.Reference(name="y"), + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Store( + base="x", + index=0, + value="y", + then=L1.Immediate( + destination="t0", + value=0, + then=L1.Halt(value="t0"), + ), + ) + + assert actual == expected + + +def test_cps_convert_term_begin(): + term = L2.Begin( + effects=[ + L2.Reference(name="x"), + ], + value=L2.Reference(name="y"), + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_term(term, k, fresh) + + expected = L1.Halt(value="y") + assert actual == expected + + +def test_cps_convert_program(): + program = L2.Program( + parameters=["x"], + body=L2.Reference(name="x"), + ) + + fresh = SequentialNameGenerator() + actual = cps_convert_program(program, fresh) + + expected = L1.Program( + parameters=["x"], + body=L1.Halt(value="x"), + ) -# assert actual == expected + assert actual == expected