Skip to content
This repository has been archived by the owner on May 31, 2020. It is now read-only.

[GSoC] Implement asyncio support #763

Open
BPYap opened this issue Mar 11, 2018 · 21 comments
Open

[GSoC] Implement asyncio support #763

BPYap opened this issue Mar 11, 2018 · 21 comments
Labels

Comments

@BPYap
Copy link
Contributor

BPYap commented Mar 11, 2018

Project Description

Python 3.4 introduced asynchronous I/O support to the Python core language. Python asyncio module provides infrastructure for writing single-threaded concurrent code using coroutines, multiplexing I/O access over sockets and other resources, running network clients and servers, and other related primitives.

This project focus on implementing asyncio’s coroutine and event loop (both timer & socket based) for voc in JVM. In order to achieve the end goal, base infrastructure such as coroutine class and specific statement such as ‘yield from’ would be implemented during first phase of the project and subsequent phases will focus in implementations of Python's asyncio library as Java API.

Successful implementation of asyncio in voc will greatly benefit toga_android development as event loop in Android API can be integrated in toga using the implemented asyncio library (similar to rubicon’s event loop for ios devices). Other than that, applications that utilize concurrent computing can be executed in Java Virtual Machine - users will be able to write concurrent functions (using asyc def/await in Python) and executes them in Java Virtual Machine after transpilation.

Testing, debugging, and documenting are integral parts of the project development. Therefore, they will be carried out continuously as the project progress. The project is broken down into three phases, with end of each phase corresponding to mentor’s evaluation.

Project Details

  1. The following Python statements will be implemented through the ast module (i.e implements the conversion of ast nodes to Java bytecode):

    • yield from
    • async
    • await
  2. Some modules can be directly compiled through voc with some minor adjustments and debugging on voc, hence I will focus on debugging voc transpiler during the weeks allocated for the following modules:

    • asyncio.base_futures
    • asyncio.futures
    • asyncio.base_tasks
    • asyncio.tasks
  3. The event loop (both timer and socket based) will be rewritten in Java, as the module base_events.py depends on too many stdlib modules in which voc currently does not support, hence providing supports for all of them may not be feasible within my allocated timeline. Here's how some functionality in Python can be translated to Java:

    • collections.deque() --> Java's Deque Interface
    • time.get_clock_info('monotonic') --> Java's System.nanotime()
    • socket.py --> Java.net's Socket (prioritizes operations required by event loop, such as setup connection, receiving and sending data operations) **

** Java does not support Unix Domain Socket natively, hence I'm planning to restrict socket's address family to IPv4 and IPv6

Working Timeline

Phase 1 (May 14 – Jun 16)

Aim
To build foundational infrastructure to support asyncio implementation

Deliverables
Coroutine class, Coroutine related Exception, yield from, async, await statement support

Main challenges
Dive into VOC internals and deal with low level Python/Java bytecode

  • Week 1 (May 14 - May 19)

    • Studies generator implementations in VOC
    • Implements coroutine class
  • Week 2 (May 21 - May 26)

    • Studies exceptions thrown by coroutine (InvalidStateError, TimeoutError)
    • Implements exception class
  • Week 3 (May 28 - June 2)

    • Implements Future class
  • Week 4 (Jun 4 - Jun 9)

    • Studies ast module, java bytecode
    • Implements yield from statement support
  • Week 5 (Jun 11 - Jun 16)

    • Implements async statement support
    • Implements await statement support

Phase 2 (Jun 18 – July 14)

Aim
To implement event loop interface in Java virtual machine

Deliverables
asyncio task, asyncio queue, event loop interface

Main challenges
Figure out reusable Python implementations and how to compile them in voc and how to link them with pre-compiled binaries

  • Week 6 (Jun 25 - Jun 30)

    • Studies asyncio’s Task
    • Implements Task class
    • Implements Task functions
  • Week 7 (July 2 - July 7)

    • Studies asyncio’s queue
    • Implements Queue, PriorityQueue, LifoQueue
  • Week 8 (July 9 - July 14)

    • Studies BaseEventLoop and AbstractEventLoop
    • Implements event loop class

Phase 3 (July 16 – August 14)

Aim
To implement event loop in Java virtual machine

Deliverables
Asynchronous timer, event loop, error handling API, Socket I/O support

Main challenges
Deal with networking and synchronization concept

  • Week 9 (July 16 - July 21)

    • Implements asynchronous timer
    • Implements event loop functions
  • Week 10 (July 23 - July 28)

    • Implements error handling API (Allows custom exceptions handler in the event loop.)
  • Week 11 (July 30 - August 4)

    • Studies TCP/UDP protocol implementations in asyncio
    • Supports socket connections for event loop
  • Week 12 (August 6 - August 14)

    • Supports low level socket operations

Project Risks

Technical Risk
There’s possibility that technical difficulty will arise during implementations of asyncio for voc. I’ve compiled a list of tasks below that I think would cause timeline slips because I might underestimated the complexities of these tasks:

  • bytecode translation (Python ast node to Java bytecode)
  • low level socket operations

I will minimize the risk by preparing/do research earlier on these topics and communicate with mentors if I think that the tasks will be delayed.
In any event that delay to workflow is inevitable, I will still complete the project and fufill my responsibility even when GSoC ends. 😊

Extra Infomation

My name
Yap Boon Peng

Email
[email protected]

GitHub
https://github.com/BPYap

Education
Nanyang Technological University, Singapore (Computer Science Undergraduate, Year 2)

Timezone
UTC +8

Relevant Experience

Notes: I didn’t include community bonding period in working timeline above because I’ll be having final exams during the period. I will not be able to commit fully during that period but I can compensate by having the community bonding few weeks earlier before my finals (if mentors are ok with it) 😊

Cheers!

@uranusjr
Copy link
Contributor

uranusjr commented Mar 11, 2018

(Passing by here) I’m extremely interested how Windows-related stuff would be handled (if there are any… I’m not familiar how much of the Java standard library can be leveraged to handle platform-specific stuff). It would be super cool if you can pull it off. Some decisions might be required in order to decide what is public API and what is not; there are quite a lot of undocumented but public-looking things in there.

@freakboy3742
Copy link
Member

@uranusjr Of the many problems with Java, cross platform is one of it's strengths. This is one project where there shouldn't be any major cross-platform issues; the behavior of networking etc in the Java standard library is consistent on all platforms Java supports.

@freakboy3742
Copy link
Member

@BPYap This timeline looks really solid to me. 3 clear deliverables; the intermediate steps make sense, and the amount of time allocated to each step seem achievable (of course, the gods are now pointing and laughing at the humans making plans... 😄).

The only pieces needed to make this a compelling final proposal are:

  1. Filling in the "more detail here" section in the project description
  2. The missing third section of the proposal: "Risks". There are a couple of pieces in this plan where you've allocated a week, any my immediate reaction is "yeah... maybe.... if it all goes well...". A good risks section makes sure we all understand the likely causes of timeline slips once we get into the implementation phase.

@BPYap
Copy link
Contributor Author

BPYap commented Mar 13, 2018

@freakboy3742 Thanks for your feedback! I finished the project description and added a risks section, let me know what you think 😄

@freakboy3742
Copy link
Member

@BPYap Honestly - this is looking pretty solid. You could probably submit it as-is and it would be a very competitive proposal.

The only suggestion I would have about making it stronger would be to discuss which parts will need to be built from scratch in Java, and which parts of Python's existing asyncio code can be re-used and cross-compiled. Are you going to have to build the entire event loop yourself? Or is the work going to be mostly debugging VOC so that we can compile the asyncio Python code?

@BPYap
Copy link
Contributor Author

BPYap commented Mar 17, 2018

@freakboy3742 I did some research this afternoon and have identified parts of asyncio library that I think could be reused:

  • asyncio.base_futures / asyncio.futures
  • asyncio.base_tasks / asyncio.tasks

For the above modules, I will be focusing on debugging voc to support their cross compilation during the weeks assigned for them.

For event loop, I plan to rewrite it in Java, as the module base_events.py depends on too many stdlib modules in which voc is lacking supports for, hence providing supports for all of them may not be feasible within my working timeline.

Some examples:

  1. Implements collections.deque() implement using Java's Deque interface
  2. Implements time.get_clock_info('monotonic') using Java's System.nanotime()

@freakboy3742
Copy link
Member

@BPYap Sure - that all makes sense. If you can integrate those details into the main proposal, that would be awesome.

One additional clarification: What about the socket layer? Are you going to be able to use Java's networking layer directly, or will you need a third party library?

@BPYap
Copy link
Contributor Author

BPYap commented Mar 18, 2018

Updated proposal. 😄

@freakboy3742 It seems Java socket class does not support Unix domain address family. Support for it requires a third party library. I'm thinking to support only the "generic" socket operation and operations specific to unix (i.e create_unix_connection) will not be implemented. So in the end, we will not have dependency on other third party library. Any thoughts?

@freakboy3742
Copy link
Member

@BPYap Ok - this is probably ready for you to submit as a final proposal to the GSoC website. I'm sure we'll continue to fine tune it if you get selected for the SoC, but you've definitely shown enough detail here to warrant solid consideration in the selection process.

@freakboy3742
Copy link
Member

This project was selected for the 2018 GSoC.

@BPYap
Copy link
Contributor Author

BPYap commented May 11, 2018

Week 1 Update ## (Temporary on hold due to schedule change)

Goal

Complete asyncio.coroutines implementations in voc [#788 Work in Progress]

Relevant source codes

Summary

What's a coroutine in asyncio?

A coroutine can be defined in 2 ways:

  1. Decorating a generator function with @asyncio.coroutine
import asyncio

@asyncio.coroutine
def hello_world():
    print("Hello World!")
  1. Using keyword async def to define a coroutine function
import asyncio

async def hello_world():
    print("Hello World!")

Generators defined as coroutines can do cool stuffs like await other_coroutine** which suspends the current coroutine and wait for other_coroutine to produce result or raise an exception. (other_coroutine return to the calling coroutine via return or raise statement.)

** Besides coroutine, await can be used in conjunction with tasks or Awaitable object, which will be explored in upcoming weeks.

Ascyncio implementation in CPython

CPython defines coroutine as a type in types.py and there's also an utility function to wrap a generator function into a coroutine function.
Meanwhile in asyncio's coroutines.py module, two wrappers are provided: a debug wrapper (called CoroWrapper) and a coroutine wrapper (for @asyncio.coroutine definition, which calls the utility function in types.py).

Tasks Checklist

Over the next couple days, I will complete the tasks below:

  • Implements CoroutineType and its utility function
  • Implements @asyncio.coroutine wrapper
  • Implements debug wrapper (CoroWrapper class)
  • Writes unit tests (could possibly make use of the debug wrapper above)
  • Implements miscellaneous functions such as __repr__, iscoroutinefunction, iscoroutine

More technical details will be added soon, stay tuned :-)

@BPYap
Copy link
Contributor Author

BPYap commented May 17, 2018

Week 2 Update

Goal

Understands and implements exception related to asyncio [#817]

Relevant source codes

Summary

There are 4 new Exception classes that are related to asyncio module and are not a part of Python built-in Exceptions:

Exception Inheritance Defined in
Error Base class for all future-related exceptions concurrent.futures
TimeoutError Inherits from Error concurrent.futures
CancelledError Inherits from Error concurrent.futures
InvalidStateError Inherits from Error asyncio.base_futures

When does the errors occur?
The exception classes listed above are primarily raised future and the task class.

  • TimeoutError is raised when a future object times out
  • CancelledError is raised when a future object is cancelled (state == _CANCELLED)
  • InvalidStateError is raised when a future object's result isn't available yet (state != _FINISHED) or set_result/set_exception method of an already finished future object (_state != _PENDING) is called again

Implementation
Implementations of exception classes are relatively straight forward. However, currently voc only support built-in exception class (correct me if I'm wrong), hence need to figure out how to link custom exceptions defined in Python to those defined in Java.

Tasks Checklist

@rockobonaparte
Copy link
Contributor

I have what seems to be a particularly nasty example that is peddling some futures around in a custom event loop so I can iteratively tick through the coroutines. It's mimicking what I ultimately want to do embedding Python with Unity3d (with a .net transpiler) . In my situation, much of the code would be calling into .net Unity3d stuff, but I've approximated it in pure Python.

It imploded when I tried to run it but I don't think anybody would have been surprised. I assume I should just sit on it for a little bit but let me know if there's a spot where you'd like me to dump it. I can jazz it up for wider consumption in some little github project.

@BPYap
Copy link
Contributor Author

BPYap commented May 26, 2018

Hi @rockobonaparte , I'm currently working on bringing in features specified in PEP 342 and PEP 380 to voc and it will take a while before I start event loop implementation. However, I’d be interested to know your approach. If you don't mind, could you explain it in higher level of details on how you are embedding Python coroutines with .net Unity3d?

Sorry for late reply, as I check in here once in a week to update my progress.

Cheers 😄

@BPYap
Copy link
Contributor Author

BPYap commented May 26, 2018

Week 3 Update

Goal

Complete generator features required by coroutine [#821, #823, #831]

Relevant resource

Summary

PEP 342: Coroutines via Enhanced Generators

By adding a few simple methods to the generator-iterator type, and with two minor syntax adjustments, Python developers will be able to use generator functions to implement co-routines and other forms of co-operative multitasking.

The syntax adjustments mentioned above are:

New generator methods:

  • send(): Allow value to be send into generator
  • throw(): Similar to send() but exception is send into generator. The generator will catch the exception at yield point
  • close(): Raise GeneratorExit exception (new exception type introduced under this proposal)

Implementation in voc
In Generator.java, a new attribute/field message is added to served as storage variable for any value sent into it. Each time send(value) is called, value will be hold in the message variable.

In ast.py, an utility function is added to parse for Yield node. The function is called in visit() before super().visit(node). If a yield node is found, the node is visited and opcodes for yield is generated. After that, its message attribute is retrieved to construct a new Name node for expression evaluation. The message is then 'consumed' and reset to None for each Name node constructed.

PEP 380: Syntax for Delegating to a Subgenerator

A syntax is proposed for a generator to delegate part of its operations to another generator. This allows a section of code containing 'yield' to be factored out and placed in another generator. Additionally, the subgenerator is allowed to return with a value, and the value is made available to the delegating generator.

In yield from x statement, x must be an iterable or evaluates to an iterable. next() is called on iterator obtained from the iterable until StopIteration.

Implementation in voc
Most of the work is done in YieldFrom visitor function. An iterator is obtained from node.value and stored temporary in virtual machine. next() is called on the iterator until it is exhausted. The return value is then yielded back to the caller.

Tasks Checklist

@rockobonaparte
Copy link
Contributor

@BPYap The current plan is to extend the voc to transpile to MSIL (Microsoft Intermediate Language)--or particularly the bytecode-compiled target version of that, since that's what the .net runtime uses. Unity 3d is technically using Mono, which means it's also running off of that. This would let me compile Python to .net and give me a fighting chance at Unity just flat-out recognizing Python as any old code you could feed it.

IronPython is a dead end since the last mature version of it only works with Python 2.7, and the 3.0 branch can't import asyncio at all. Unity also wouldn't directly recognize it as something it can use; I'd have to subclass MonoBehaviour and put a bunch of overhead in place. Since the coroutines are what I'm after, it's a no-go.

My reason for a custom scheduler is so I can manually tick it instead of delegating control of my thread to it. Unity has to keep running the engine loop and can't surrender itself to a blocking coroutine scheduler. I'd treat the coroutine scheduler as a subsystem and it would just get ticked at least every frame. I could add smarts as necessary if this isn't enough and I need to schedule more intelligently.

I'm very much all-in on coroutines because there's a lot of sequential game logic that is written much more concisely with coroutines than, say, having to drag a bunch of game state around to make some body of code pseudo-reentrant. Unity's built-in technique for this are kind of gross; it relies IEnumerators, which would be akin to writing coroutines just using old-school yield statements in Python.

Anyways you don't want to be waiting on me here. I just got as far as discovering the visitor code in voc.python.ast and I see I have my work cut out for me. I'm currently trying to collapse some of that code into a common helper to reduce the FUD of trying to write a .net equivalent of ~2000 lines of code in one file.

@BPYap
Copy link
Contributor Author

BPYap commented Jun 10, 2018

Week 4 & 5 Update

Goal

Support raising and catching of custom exception [#847]

Relevant resource

Summary

Currently voc only recognizes built-in Python exceptions defined in org.python.exceptions. During node traversal, it appends org.python.exceptions as default prefix for all exception names, hence custom exception that is not defined in org/python/exceptions will cause java.lang.NoClassDefFoundError and custom exception imported from other modules will cause AttributeError: 'Attribute' object has no attribute 'id' during transpilation. In fact, this is one of the main reasons why certain modules in ouroboros are not able to compile-- they are referencing custom exceptions defined in other modules.

In the case of asyncio modules, here are the files and reasons that caused transpilation error:

  • base_event.py (caused by CancelledError from concurrent.futures)
  • locks.py (caused by CancelledError from concurrent.futures)
  • proactor_events.py (caused by CancelledError from concurrent.futures)
  • selector_events.py (caused by socket.error from socket)
  • sslproto.py (caused by various SSL errors from ssl)
  • tasks.py (caused by CancelledError from concurrent.futures)
  • window-events.py (caused by CancelledError from concurrent.futures)

Since all of the errors are related to custom exceptions, solving the custom exceptions problem in voc should result in successful compilation of entire asyncio module in voc, which means we probably won't have to implement asyncio module in Java.

Implementation in voc
As discussed with @freakboy3742, I will write a node visitor class to traverse the imported modules to discover full classref of custom exception class and add them to self.symbol_namespace of main visitor class. This would overwrite the default_prefix of org.python.exceptions. I will also implement handling of attribute node in visit_Raise and visit_ExceptHandler.

Tasks Checklist

@BPYap
Copy link
Contributor Author

BPYap commented Jul 1, 2018

Week 6 & 7 Update

Goal

Investigating and fixing bugs occurred when importing asyncio module through voc [#849, #854]

Relevant resource

Summary

Aside from custom exception importing, the other two problems causing asyncio module to fail when attempting to import it in voc were Unknown constant type <class 'frozenset'> in function definition error and lack of nonlocal statement support.

Issue 1
Unknown constant type <class 'frozenset'> in function definition error occurs during Block.add_tuple() in voc/python/blocks.py. It appears that if a set is not bind to any variable (e.g: 1 in {1, 2, 3, 4, 5}), python interpreter treats that set as a constant (of fronzenset type since there is no way to modify the un-bind set), and it is passed to Block.add_tuple() during transpilation. Since frozenset type is not handled in the logic of Block.add_tuple(), voc transpiler raises RuntimeError: "Unknown constant type frozenset and exits.

*Side notes: While solving this issue, I encountered two different bugs related to set data type and they are documented in issues #857 and #859. For now these bugs shouldn't cause issues on asyncio imports since none of the files in asyncio modules are manipulating set in such a way that would reproduce the bugs.

Implementation in voc
Solving this issue is pretty straight forward as we just have to handle fronzenset constant type in add_tuple by adding opcodes to construct a Python set object containing values of the frozenset.

Issue 2
tasks.py in asyncio is using nonlocal statement which is currently not supported in voc. Looking things in a short term, it is easier to get around this by modifying the source code directly and replacing the modified source code with the existing one in ouroboros repository. However, in a longer term it would be more appropriate to bring support of nonlocal to voc.

In summary, the nonlocal statement causes the listed identifiers to refer to previously bound variables in the nearest enclosing scope excluding globals, and the listed identifiers must not collide with pre-existing bindings in the local scope. This means modifying nonlocal variable in inner scope will modify the variable in nearest enclosing scope as well since they are pointing to same memory location.

Implementation in voc
To implement nonlocal support in voc, the closure object maintains a list of outer scope (context).

  1. When the value of nonlocal variable changed, its new value is stored temporarily in global space.
  2. During loading of values in outer context, the context checks the global space for modified value. If it owns a variable and it is found in the global space, the context updates the variable with the modified value from global space before placing it on stack.
  3. Modified variables in global space are identified with id of context that owns it and the entry is deleted once the context has updated its variables.

Tasks Checklist

@BPYap
Copy link
Contributor Author

BPYap commented Jul 10, 2018

Week 8 Update

Goal

Solve regression issues in #854

Relevant resource

Summary

This week mainly focus in re-implementing nonlocal statement and make it to work in voc without causing regression issues. See below for implementation details.

Keeping track of nonlocals

To keep track of which variables are nonlocal, a new member variable nonlocal_vars (a Python list) is introduced to the Accumulator class in python/blocks.py.

When nonlocal node is hit during ast traversal, all nonlocal names are appended into nonlocal_vars variable of current context. Example psudocode of loading/storing variables thus become:

  1. If the variable's name is found in local_vars, load/store its value from/into locals.
  2. If the variable's name is found in non_local_vars, load/store its value from/into locals of owner context**.
  3. Otherwise load from/store into globals.

** owner context refers to the enclosing context (scope) that owns the nonlocal variable.

Determining owner context

To load/store nonlocal from owner context, we first need to determine the owner context given current context. To do that, another new member variable outer_scopes (a Python list) is added to Function, Method, Generator, GeneratorClosure, Closure context (basically every context that inherits from Function context). During add_function and add_class, current context is appended to the newly created function/class's outer_scopes variable. Using this outer_scopes, we are able to determine the owner context when loading/storing nonlocals using the algorithm below:

  1. Reverse current_context.outer_scopes so that the most inner context/scope is processed first.
  2. Iterate through the reversed outer_scopes.
  3. For context in each iteration, if name of variable is found in context.local_vars, go to step 4, else go to step 2.
  4. If context is instance of Closure and name of variable is found in context.klass.closure_var_names**, go to step 2, else return context as owner context.

**closure_var_names holds the variables from co_freevars of code object, hence if variable name is found in it, the variable is from somewhere else in the outer scopes.

Communicating between different context

When we have pinpointed owner context for our nonlocal variable, we need a way to load/store from the owner context. At first glance, it seems calling owner_context.store_name(name) or owner_context.load_name(name) directly from the inner context would do the job. However, the sequence of opcodes matters, so the approach above would not work because we don't exactly know the 'layout' of owner_context's opcodes when we are in inner context.

Therefore a 'middle-man' lookup table is required to exchange variable's value between inner scope and outer scope. Under org/python/types/Module.java, a new field closure_var_names ( org.python.types.Dict) is added. This field keeps all closure variables of all contexts in current module. The variable names are identified by appending id of owner context at the end of the name.
During add_function or add_class, closure variables are stored into closure_var_names. By adding all closure variables (not just nonlocals), we are able to load closure variables in method and generator, which is not possible before.

To load nonlocals and closure variables (for method and generator) from inner context, we can retrieve its value from closure_var_names. Conversely, to store nonlocals and closure variables, we just update the entry in closure_var_names and signal the owner_context to update its locals when it calls load_name.

Tasks Checklist

@BPYap
Copy link
Contributor Author

BPYap commented Aug 4, 2018

Week 10 & 11 Update

Goal

Continue nonlocal implementation and finding out which modules are causing issues when importing asyncio module.

Summary

This week we are trying out new way to implement nonlocal and closures by passsing in locals of parent context as reference into closure object and implementing several data structures from collections in Java.

The issue with extra variables to keep track of nonlocals

Previously we are using a field variable in Type.java to store nonlocal variable temporarily when it is updated/modified by closure. This approach is risky and 'hacky' because when we are passing data around in runtime, leaking of function state might happen (e.g. two function calls might share the same temporary storage).

Fortunately, there is this #locals variable that was introduced when generator is added to voc. #locals is a Java Hashmap that keeps track of local variables stored in local registers of a function call frame. It is originally used to restored generator state when the generator is restored. We can make use of this variable to achieve nonlocal behavior by passing it as reference when we are creating closure. That way, when closure has modified a nonlocal variable, it updates the HashMap reference directly, which implies that no extra storage space is needed since both parent context and closure context are sharing the same HashMap reference. The closure will maintain a list of HashMap if it is nested in several layers of parent functions. The order of elements in that list are ordered from most inner parent to most outer parent.

Issues when importing asyncio module

When nonlocal is sorted out, the next issue arised when attempting to import asyncio module through voc are binary dependencies in collections module. On deeper inspection, asyncio depends on defaultdict, Deque and Ordereddict data structures defined in collections module. While OrderedDict is implemented in pure Python, defaultdict and Deque are implemented in C. Therefore, we will have to implement it in pure Python in ouroboros or implement it as Java API. Since those data structures have good analog in Java (Deque vs java.util.Deque and OrderedDict vs java.util.LinkedHashMap), it is more preferable to implement them as Java API.

Tasks Checklist

@BPYap
Copy link
Contributor Author

BPYap commented Aug 14, 2018

Week 12 Update

Well, that's a wrap for GSoC 2018. 😄

Huge thanks to @freakboy3742 and BeeWare project for an amazing GSoC experience. For summary of what I did in GSoC 2018, visit https://pybee.org/news/buzz/2018-google-summer-of-code-final-report-yap-boon-peng/.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests

4 participants