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

[demo] Add reification in src/Experiments/SimplyTypedArithmetic.v #275

Merged
merged 9 commits into from
Nov 26, 2017

Conversation

JasonGross
Copy link
Collaborator

It's rather verbose, unfortunately. The reification also doesn't have
any of the nice debugging features of the version of reification in
Compilers, because that's even more boilerplate. Not sure if I should
add that back in, at the moment.

Also, for some strange reason, places where constrs fail to typecheck
seem to induce backtracking where I don't think they should, and I'm not
sure what's going on...

@JasonGross JasonGross added this to To Do in Pipeline without CPS via automation Nov 24, 2017
Copy link
Contributor

@andres-erbsen andres-erbsen left a comment

Choose a reason for hiding this comment

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

Overall strategy looks good as expected. I still scribbled all over this, mostly to make sure I am actually reading it. I think the machinery for substituting type arguments during reification warrants some more explanation. I would also like you to consider the namespacing suggestions I made. However, don't try to address every individual scribble that I made, just take the overall sense of what caused me to do a double-take into consideration. If I don't respond for a day after you make changes (or decide not to), feel free to merge this anyway and move on with whatever you are doing.

@@ -178,22 +179,435 @@ Module Positional. Section Positional.
End mulmod.
End Positional. End Positional.

Import Associational Positional.
Module Compilers.
Inductive type := Unit | Prod (A B : type) | Arrow (s d : type) | List (A : type) | TNat | TZ | TBool.
Copy link
Contributor

Choose a reason for hiding this comment

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

What are your thoughts on putting constructors in a namespace named after the type, as in Lean? I think I have an easier time remembering Compilers.type.nat vs Compilers.TNat (and obviously I would import Compilers if I used it alot).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, seems fine. I'll do that. Should I also call it type.interp rather than interp_type?

Import Associational Positional.
Module Compilers.
Inductive type := Unit | Prod (A B : type) | Arrow (s d : type) | List (A : type) | TNat | TZ | TBool.
Delimit Scope ctype_scope with ctype.
Copy link
Contributor

Choose a reason for hiding this comment

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

What does ctype stand for?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

c stands for compilers or compiler or compiled. The issue is that type_scope is already taken by Coq, and scopes aren't namespaced.

Inductive type := Unit | Prod (A B : type) | Arrow (s d : type) | List (A : type) | TNat | TZ | TBool.
Delimit Scope ctype_scope with ctype.
Bind Scope ctype_scope with type.
Notation "()" := Unit : ctype_scope.
Copy link
Contributor

Choose a reason for hiding this comment

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

Would ( ) parse as Unit?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No, but I can fix that. Shall I?

| BoolRect {T} : op (T * T * TBool) T
| NatRect {P} : op (P * (TNat -> P -> P) * TNat) P
| Seq : op (TNat * TNat) (List TNat)
| Repeat {A} : op (A * TNat) (List A)
Copy link
Contributor

Choose a reason for hiding this comment

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

I read Repeat as while (1) { ... } at first. Hmm. ListRepeat could be an ad-hoc solution, though the real issue here is lack of namespacing of op values themselves...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Renamed to List_repeat, added some namespacing

| Zmodulo : op (TZ * TZ) TZ
| Zeqb : op (TZ * TZ) TBool
| ZofNat : op TNat TZ
| App {s d} : op ((s -> d) * s) d.
Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, ListApp is above already...

end) in
lazymatch rest with
| (fun _ => ?rest)
=> constr:((tt, rest))
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this function just return a (tt, (tt, (tt, ...))) according to the arity of the input? If so, why is it useful?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes. It's useful because some other code relies on the "delayed arguments" list exactly matching the shape of the type. We could instead add some extra cases to deal separately with tt as dummy, and (tt, _) as dummy, but I decided to pay the cost in this one place, rather than in all the places where I use it.

The problem I'm solving is that some applications need to stay as Gallina applications, and not turn into PHOAS application nodes. I could throw an error if not all such applications came at the head of the term (i.e., if there was anything of type nat -> forall T : Type, ....), but I currently support such cases. So I need to save the arguments that need to be plugged in as Gallina applications, and mark the ones that become PHOAS applications.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually support these, though? I don't know what would be passed to reify_op for something like this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

These = nat -> forall T : Type, ...? Or just type arguments? For type arguments, we need something like this for List.map, etc. For things like nat -> forall T : Type, ..., we nominally support them. We can reify something silly like

(fun (n : nat) (T : Type) (f : T -> nat) (x : T) => n + f x) 0 nat (fun x => x) 1

But we don't need to support these things. Would it be better to drop support for them?

Copy link
Contributor

Choose a reason for hiding this comment

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

Oh, so this is about being insensitive to ordering of arguments / section variables. That seems like a good property.

| (tt, ?delayed_arguments) => check_delayed_arguments_dummy delayed_arguments if_dummy if_not_dummy
| (_, _) => if_not_dummy ()
end.
Ltac plug_delayed_arguments f delayed_arguments :=
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, I now understand here that we are doing substituting in some arguments of a function and not others. However, it is not clear how the bindings being substitutied are encoded. Part of this might be me having trouble reading tactics-in-terms.

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 encoding is that there is a tuple of arguments. tt means "this becomes a PHOAS application node", anything else means "this should be substituted in Gallina". (This is safe, because only things of type Type, Set, Prop, or _ -> _ -> ... _ -> {Type,Set,Prop} get substituted.) Terms are eta-expanded as necessary to fill in arguments, but, importantly, are not eta-expanded if all remaining arguments are dummy (tt). Perhaps there is a cleaner way to do all of this.

Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we pass arguments in tuples anyway? Why does that transformation need to happen during reification?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

There are two cases:

  1. Somehow, reification needs to know to reify @List.map ?A ?B, not @List.map. This could be solved by cps-ing reify_op, and then having reification try reify_op first, and only try other things if reify_op fails. However, this will stop working if we want to, e.g., define List.map in terms of list_rect and remove List.map from reification in favor of an initial cbv delta [List.map], in which case we'll have:
  2. reification of (fun (A B : Type) => ...) T U

Because we're not doing cbv beta, we need to either commit to never having lambdas that take type arguments, or we need to pass types around in tuples to do controlled beta reduction ourselves, or we need to write special cases for all of the numbers and kinds of type arguments we want to handle.

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that we do need to implement beta reduction of type arguments.

Uncurry0 (@OpConst rT term)
end.

Inductive context_var_map {var : type -> Type} :=
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, how about namespacing the constructors? I actually had to look them up when I encountered them below.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

| ?f ?x
=>
let x_ty := type of x in
let x_type_arg := is_type_arg x_ty in
Copy link
Contributor

Choose a reason for hiding this comment

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

is_type_arg -> is_type or is_sort or something like that?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, basically

lazymatch x_type_arg with
| true
=> (* we can't reify things of type [Type], so we save it for later to plug in *)
reify_helper var f ctx (x, delayed_arguments)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is delayed_arguments a list of substitutions to be made during reification?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, with tt marking places where substitutions are reified as applications rather than made

@JasonGross
Copy link
Collaborator Author

Eh, responding to the scribbles was not hard. I appreciate them. I think you did a double-take at all of the places where my abstraction barriers are leaky, painful, or unclean. There are still some design questions to be answered (around currying of functions and around what the right interface is for delayed arguments), so I'll wait on your feedback. (Note also that I made delayed arguments more general than it needs to be for me to use it here. Perhaps I should just assume/enforce the invariant that all type arguments come first, and that will simplify code a decent amount?)

Notation op := op.
End Notations.
End op.
Export op.Notations.
Copy link
Contributor

Choose a reason for hiding this comment

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

is Export actually necessary here? could the alias just be after the end of the module?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Alias could be after the end of the module, but I imagine op may get it's own file, or something, and this is preempting that. Is that a bad idea?

let n' := fresh n' in
n'.

Ltac is_type_arg arg_ty :=
Copy link
Contributor

Choose a reason for hiding this comment

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

Why do we need to do beta-reduction here at all?

I would rename to is_template_parameter parameter_type (even though we call it on argument types).

end) in
lazymatch rest with
| (fun _ => ?rest)
=> constr:((tt, rest))
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we actually support these, though? I don't know what would be passed to reify_op for something like this.

JasonGross added a commit to JasonGross/fiat-crypto that referenced this pull request Nov 25, 2017
@andres-erbsen
Copy link
Contributor

I am still confused why we need to mark non-template arguments with tt -- they should be obvious from the function itself. Is this somehow because we do currying in the same pass?

@andres-erbsen
Copy link
Contributor

I think I am now convinced that the reification should indeed be doing what it is doing right now. However, it seems sketchy that we need to insert tt in the list of type arguments to keep things in sync -- I am guessing it is duplicating some other information already available in the context where it is used, and the code would be nicer without it. E.g. we could just keep an ordered list of template arguments, using them on demand.

reify_rec (@Let_In A B a b)
| ?f ?x
=>
let x_ty := type of x in
Copy link
Contributor

Choose a reason for hiding this comment

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

actually, we could just inspect the type of f here instead of x to make parameter/argument naming less confusing.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, that seems wise, I'll do that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

| (fun x : ?T => ?f)
=>
(*let dummy := match goal with _ => idtac "funcase delaying" delayed_arguments end in*)
lazymatch delayed_arguments with
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we branch on T here instead, only consuming the arguments list when T is a template parameter?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, though then I should change cbv beta to something stronger like hnf, because we need to avoid the case where the type of the binder unifies with the type of the argument, but is syntactically different in a way that causes us to delay the argument and then miss it here, or vice versa (e.g., if we ever do Definition Type1 := Type.).

Copy link
Contributor

Choose a reason for hiding this comment

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

Could we change to consistently inspecting the function binder type, not the argument type?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure, but that's not enough for all cases (e.g., ((fun (T : Type) (x : T) => x) : forall T : Type1, T -> T) nat 0)). I think I have a decent way of switching to using unify, though.

Copy link
Contributor

Choose a reason for hiding this comment

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

Okay, it really does seem that alias unification (in the same sense as it came up for section records) is what we want here. I think I might be okay with just not supporting aliases for Type and Set, though. Also happy to look at your unify solution.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Changed

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

JasonGross added a commit to JasonGross/fiat-crypto that referenced this pull request Nov 26, 2017
@JasonGross
Copy link
Collaborator Author

E.g. we could just keep an ordered list of template arguments, using them on demand.

Updated to do that

first [ unify parameter_type Prop
| unify parameter_type Set
| unify parameter_type Type
| lazymatch eval hnf in parameter_type with
Copy link
Contributor

Choose a reason for hiding this comment

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

Could we ask for more fine-grained reduction here by trying to unify with the match pattern instead of calling hnf and then doing a syntactic check?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

unify doesn't work on uconstrs, so unify parameter_type (_ -> _) will always fail. I could make an open_constr first, but that's clunky and not exactly fine-grained; I guess one way is:

let T := open_constr:(_) in
let arrow := open_constr:(_ -> T) in
unify parameter_type arrow;
require_template_parameter T

Is that better?

Copy link
Contributor

Choose a reason for hiding this comment

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

What is the difference beteewen uconstr and openconstr? What does the change from one to another require? Is using an arrow instead of a forall intentional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If I write unify p (_ -> _), _ -> _ gets interpreted as a ... constr, I think? The implicit cast from uconstr to constr requires that all holes be fillable by type inference. The "cast" from uconstr to open_constr fills uninferrable holes with evars. (constr and uconstr roughly correspond to actual datatypes, while open_constr is just a description of how to fill holes, roughly) What I am saying here is that I need the extra two let lines because the default unify syntax will try to fill the holes too early.

The change from a forall to an arrow is suboptimal, but we already can't handle any of the cases where I can imagine it coming up (things that look like recursion principles of length-indexed lists). If we use forall, then we need to reintroduce tactics in terms. I'm confused by your objection to hnf + match, though; can you add a bit more?

Copy link
Contributor

Choose a reason for hiding this comment

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

hnf is one of the things I assume to be arbitrarily slow (in case the next match fails, in particular). Unification too, but I am less angry at unification in types. Feel free to change back to hnf if you feel better about it.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

hnf is differently arbitrarily slow than unification. (Note that I am doing hnf in a type, here. It'd probably even be safe to do cbv / compute.) In particular, if unify is faster than hnf in this case, it will be because unify performs more beta reduction and leaves over fewer things of the form (fun _ => <small term>) <big term>. In this case, I'm making use of the fact that I absolutely need to know whether or not the term is headed by forall, and that hnf stops evaluating when it hits forall.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, I think what I was saying is that we don't need to know what the head symbol is if it is not a function type constructor. Use your judgement for which command to use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Sure. I'm leaving it as hnf. I think I see the algorithm you're proposing (first check if it's a forall, which should be fast, since you only need a very limited form of beta, then, if it is, pull out the type). However, there isn't actually a primitive for this, and unify doesn't follow this strategy, I believe.

idtac
end ].
Ltac is_template_parameter parameter_type :=
run_tactic_as_bool ltac:(fun _ => require_template_parameter parameter_type).
Copy link
Contributor

Choose a reason for hiding this comment

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

run_tactic_as_bool -> is_success or is_succeeding or sth?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Mmm, I'm not sure about this. The semantics are that it runs a tactic during tactic expression evaluation, and returns a constr indicating success or failure. I use it to build a more general constr_tryif_then_else:

(** Runs a tactic during expression evaluation phase; on success,
returns [then_tac ()], otherwise returns [else_tac ()] *)
Ltac constr_tryif_then_else tac then_tac else_tac :=
let success := run_tactic_as_bool tac in
lazymatch success with
| true => then_tac ()
| false => else_tac ()
end.

I could use this (though it may be a bit awkward). Or maybe is_success_run_tactic?

Copy link
Contributor

Choose a reason for hiding this comment

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

is_success_run_tactic sounds okay, I also don't really care

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done


Ltac plug_delayed_arguments f delayed_arguments :=
lazymatch delayed_arguments with
| tt => f
Copy link
Contributor

Choose a reason for hiding this comment

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

Is tt acting as a list nil here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

yes

Copy link
Contributor

Choose a reason for hiding this comment

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

Please document the delayed_arguments substitution context representation in a comment.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

constr:(I : I)
end
end
| _
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment explaining why this needs to be the default case.

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'm not sure what to say in the comment. "This must come last because we're using lazymatch"? Or "This must be here because we do in fact need to reify [op]s"? Or something else? I'm not sure what I'm explaining: that this case is present, that it's last, that it's run whenever the only cases don't match, that it's run only when the other cases don't match, or that there's no other cases to handle when none of the others match (or something else)

Copy link
Contributor

Choose a reason for hiding this comment

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

Why can't we write an actual pattern in this case, instead of _? Why do we need to use a catch-all instead of something more specific.

lazymatch rf0 with
| (fun _ => ?rf)
=> constr:(@Abs var rT _ rf)
| _ => let dummy := match goal with
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add a comment explaining when this case might happen.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

end
end.

Ltac reify_helper var term ctx delayed_arguments :=
Copy link
Contributor

Choose a reason for hiding this comment

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

Proposed rename: value_ctx and template_ctx

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

reify_rec (@Let_In A B a b)
| ?f ?x
=>
let ty := get_first_argument_type f in
Copy link
Contributor

Choose a reason for hiding this comment

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

type_of_first_argument_of

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

@andres-erbsen
Copy link
Contributor

This might be the last set of comments I have. I think at this point handling of template-arguments is essentially as systematic as the handling of runtime arguments in ctx, and what remains is to make that convention obvious from code and comments.

It's rather verbose, unfortunately.  The reification also doesn't have
any of the nice debugging features of the version of reification in
Compilers, because that's even more boilerplate.  Not sure if I should
add that back in, at the moment.

Also, for some strange reason, places where `constr`s fail to typecheck
seem to induce backtracking where I don't think they should, and I'm not
sure what's going on...
We no longer pass around dummy markers in the tuple of arguments.
This time without exponential slowdown in failure cases and without
needing to manually think up all of the possible errors and write them
out.

Possible thanks to Hugo's comment at
coq/coq#6252 (comment)
| forall x : ?T, _ => T
end.

(** Template parameters (things whose type or codomain is [Type],
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggestion: Forms of abstraction in Gallina that our reflective language cannot handle get handled by specializing the code "template" to each particular application of that abstraction. In particular, type arguments (Type, Set, Prop) get substituted into lambdas and treated as a integral part of primitive operations (such as @List.app T). During reification...

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Updated to

-  (** Template parameters (things whose type or codomain is [Type],
-      [Set] or [Prop]) get handled specially, because our PHOAS
-      representation does not support dependent types.  During
+  (** Forms of abstraction in Gallina that our reflective language
+      cannot handle get handled by specializing the code "template" to
+      each particular application of that abstraction. In particular,
+      type arguments (nat, Z, (λ _, nat), etc) get substituted into
+      lambdas and treated as a integral part of primitive operations
+      (such as [@List.app T], [@list_rect (λ _, nat)]).  During

=> constr:(@Abs var rT _ rf)
| _
=> (* This will happen if the reified term still
mentions the non-var variable. By chance, [cbv delta]
Copy link
Contributor

Choose a reason for hiding this comment

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

What do casts have to do with this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you do something really silly like try to reify (fun x : nat => S (O : (fun _ => nat) x)), by chance, cbv delta strips away the cast and has us reifying S O under the binder. If instead we passed S (O : (fun _ => nat) x) to reify_op, the term we get back is @op.Const type.nat (S (O : (fun _ => nat) x)). This term would trigger this error message, because it would still mention the binder x.

I can't think of any other way to get x into the reified term, because we pull everything except for constants apart to reify them, but that doesn't mean I thought of everything.

@andres-erbsen
Copy link
Contributor

Feel free to change comments as you wish / in response to things I may have said and forgotten about, but I think it is also fair to call this done. I am actually really happy with how this turned out, including the initial version which was much more digestible than what I was afraid of at last meeting.

@JasonGross JasonGross merged commit 499b76f into mit-plv:master Nov 26, 2017
Pipeline without CPS automation moved this from To Do to Done Nov 26, 2017
@JasonGross JasonGross deleted the reification-in-demo branch November 26, 2017 23:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
No open projects
Development

Successfully merging this pull request may close these issues.

None yet

2 participants