Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multistage programming #618

Merged
merged 15 commits into from
Feb 4, 2025
Merged

Multistage programming #618

merged 15 commits into from
Feb 4, 2025

Conversation

kehemo
Copy link
Collaborator

@kehemo kehemo commented Apr 17, 2024

No description provided.

@kehemo kehemo marked this pull request as draft April 17, 2024 05:34
@kehemo
Copy link
Collaborator Author

kehemo commented Apr 17, 2024

Python is the metalanguage and the Exo base language has not been changed in any breaking way

Python unquote blocks are evaluated using exec. Each unquote behaves like a new scope in python. Both unquote and quote blocks have parent scopes that look at the nearest enclosing block of the same type. For example:

@proc
def foo():
    a: i8
    with unquote:
        b = 1
        with quote:
            a = 1
            with unquote:
                print(b)

When this procedure is evaluated, it will print "1", and then result in a procedure that is equivalent to:

@proc
def foo():
    a: i8
    a = 1

@codecov-commenter
Copy link

codecov-commenter commented Apr 17, 2024

Codecov Report

Attention: Patch coverage is 98.44560% with 6 lines in your changes missing coverage. Please review.

Project coverage is 88.23%. Comparing base (d48aa36) to head (e09d7eb).
Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/exo/frontend/pyparser.py 97.62% 6 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #618      +/-   ##
==========================================
+ Coverage   88.07%   88.23%   +0.15%     
==========================================
  Files          92       93       +1     
  Lines       21303    21625     +322     
==========================================
+ Hits        18763    19080     +317     
- Misses       2540     2545       +5     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@kehemo
Copy link
Collaborator Author

kehemo commented Apr 17, 2024

Some relatively minor things are still broken:

  • I make the assumption that free variables in Python cannot be mutated. This is not true in the full semantics of Python due to the nonlocal keyword.
  • I do not check the AST when mangling some identifiers that I splice into the code. I only check variables from enclosing scope in that case
  • I allow users to write return statements inside unquote blocks, which kinda doesn't make sense

@kehemo
Copy link
Collaborator Author

kehemo commented Oct 30, 2024

This branch is only a subset of the desired features for metaprogramming. In future PRs, consider:

  • adding an API for reflection on Exo statement objects and expressions
  • adding macros (functions outside of exo procs that can be used in unquoted blocks)
  • adding syntax that lets users create an Exo identifier and unquote it in any position an Exo identifier would appear

@kehemo kehemo marked this pull request as ready for review October 30, 2024 04:45
@kehemo kehemo requested a review from yamaguchi1024 October 30, 2024 04:45
@yamaguchi1024
Copy link
Member

Thanks Kenneth! I will review this PR more carefully, but meanwhile can you write a documentation for metaprogramming features and limitations? (see #729 for sample documentation for other features).

y: f32
y = sin(x)


def test_sin2():
@proc
def sin(x: f32):
def sin_proc(x: f32):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Was this change necessary? Is it because the procedure name is conflicting with the extern name?
I think procedure definitions should be able to overwrite whatever other global and local variables out there

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The change is necessary because the sin function call inside the proc is parsed as an implicit unquote in the metalanguage, which means it is looked up like a normal python variable named sin would be looked up at that point. But, if the proc is named sin, it is registered as a local variable in the scope which shadows the sin extern that is imported as a global variable.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmmm I see what you mean but this seems quite cryptic.... It's way too opaque to users... How bad is it going to be if we don't support implicit unquotes?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code becomes pretty cluttered if there are no implicit unquotes because you will have to put braces around every variable you reference from the metalanguage.

Also, I think it should be expected that if you name a proc "sin" it will shadow any other "sin" inside its scope. In most other languages, you can refer to the function you are currently inside by its name (e.g. recursion), so it doesn't seem very surprising.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a side note, if you try compiling this exo:

@proc
def sin(x: f32):
    x = sin(x)

the result is:

#include <math.h>
// sin(
//     x : f32 @DRAM
// )
void sin( void *ctxt, float* x ) {
*x = sin((float)*x);
}

which is actually invalid C because the sin inside the function will refer to the proc sin not the function from math.h.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see okay, this makes sense. Can you add a test case of declaring a proc named sin that shadows the extern sin, and call the proc sin from another procedure foo, and see if that works as expected?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complete

with exo:
a = {bar(~{a + 1})}

c_file, _ = compile_procs_to_strings([foo], "test.h")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you compiling the proc into C for every test? I think you should also sanity-check the ExoIR printing.
Can modify the asserts to sth like:

assert str(foo) + c_file == golden

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, you know that you can update all the golden tests by running pytest tests/test_metaprogramming.py --update-golden , right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

complete

@@ -15,6 +15,9 @@
from ..core.prelude import *
from ..core.extern import Extern

from typing import Any, Callable, Union, NoReturn, Optional
import copy
from dataclasses import dataclass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are those imports? Are they necessary? If so, you should add them as dependencies to requirements.txt

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The imports are parts of the python standard library just to give some type annotations and reduce some clutter

@@ -35,54 +38,106 @@ def str_to_mem(name):
return getattr(sys.modules[__name__], name)


@dataclass
class SourceInfo:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you need a wrapper for SrcInfo?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I decided to make a wrapper because before we just passed a getsrcinfo function into Parser, which I thought was confusing because then you don't know where the behavior of getsrcinfo is defined. I think it is neater if it is wrapped in an object with a defined get_src_info method that you can jump to.

srcinfo = self.getsrcinfo(e)
else:
srcinfo = self.getsrcinfo(node)
if isinstance(e, pyast.Slice):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add tests to cover the lines Codecov is reporting?



@dataclass
class QuoteReplacer(pyast.NodeTransformer):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should definitely add docstrings to each Class and functions you defined

if isinstance(unquoted, str) and unquoted in _prim_types:
return _prim_types[unquoted]
else:
self.err(node, "Unquote computation did not yield valid type")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need else: on if len(unquote_eval_result) == 1: and raise error

)
else:
return tuple()

def eval_expr(self, expr):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add tests for

mems=[DRAM, ...]
x : f32 @ mems[0]

@kehemo
Copy link
Collaborator Author

kehemo commented Jan 15, 2025

admittedly the docs could use better motivated examples, but I found it difficult to include any without overly complicating what should also serve as a reference that is easily skimmable. I think the first limitation (lack of quote outside of exo procs) also holds metaprogramming back by a lot more than i originally anticipated, but we should try to merge the existing features first and then hopefully reduce the limitations later.

I considered documenting how metaprogramming works internally in pyparser, but besides my existing comments, it doesn't really seem like there is a space for a more in-depth explanation.

@kehemo
Copy link
Collaborator Author

kehemo commented Feb 3, 2025

Implicit quote/unquote lookup now progressively looks up enclosing scopes without discerning whether it is a Python or Exo scope. Thus, Python variables can shadow Exo variables if the enclosing Python scope is closer to the reference.

I found that this change is easier to generalize if I also allow implicit unquotes on the left hand side of assignments. For example, in this program:

@proc
def foo(a: i32):
  with python:
    with exo:
      a += 1

a is not defined in the with exo: scope, but rather an enclosing scope. So, with the scoping changes, it is more natural to lookup a by deferring to the enclosing with python: scope, which then finally looks up the name in the top scope. However, by passing through the with python: scope, this name lookup will return a Python object that has to be unquoted. So, I created a new class of object code called ExoSymbol, which can be unquoted as both an expression and a left hand side variable. Implicit quotes will always return an ExoSymbol so something like this is also now possible:

@proc
def foo(a: i32, b: i32):
    with python:
        syms = [a, b]
        for sym in syms:
            with exo:
                sym += 1

@kehemo kehemo merged commit 4bd40ca into main Feb 4, 2025
9 checks passed
@kehemo kehemo deleted the metaprogramming branch February 4, 2025 16:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants