Compare commits

...

3 Commits

Author SHA1 Message Date
Smaug123
1e1176bec5 Generate instruction DU 2024-02-16 11:33:06 +00:00
Smaug123
16daa1b7ca Start working on cata generator 2024-02-16 00:23:13 +00:00
Smaug123
ef4a83ae61 Bones 2024-02-15 01:04:26 +00:00
6 changed files with 632 additions and 0 deletions

View File

@@ -0,0 +1,180 @@
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
type Const =
| Int of int
| String of string
type PairOpKind =
| NormalSeq
| ThenDoSeq
[<CreateCatamorphism>]
type Expr =
| Const of Const
| Pair of Expr * Expr * PairOpKind
| Sequential of Expr list
| Builder of Expr * ExprBuilder
and [<CreateCatamorphism>] ExprBuilder =
| Child of ExprBuilder
| Parent of Expr
// Say that CreateCatamorphism-tagged types form the set T.
// Assert that each U in T is a discriminated union.
// For each type U in T, assign a generic parameter 'ret<U>.
// For each U:
// * Define the type [U]Cata, generic on all the parameters {'ret<U> : U in T}.
// * For each DU case C in type U:
// * create a method in [U]Cata, whose return value is 'ret<U> and whose args are the fields of the case C
// * any occurrence in a field of an input value of type equal to any element of T (say type V) is replaced by 'ret<V>
// Finally, define a type Cata<{'ret<U> for U in T}>
// with one member for each U, namely of type [U]Cata<{'ret<U> for U in T}>.
type ExprCata<'builderRet, 'ret> =
abstract Const : Const -> 'ret
abstract Pair : 'ret -> 'ret -> PairOpKind -> 'ret
abstract Sequential : 'ret list -> 'ret
abstract Builder : 'ret -> 'builderRet -> 'ret
type ExprBuilderCata<'builderRet, 'ret> =
abstract Child : 'builderRet -> 'builderRet
abstract Parent : 'ret -> 'builderRet
type Cata<'bret, 'ret> =
{
Expr : ExprCata<'bret, 'ret>
Builder : ExprBuilderCata<'bret, 'ret>
}
// Then we can create the noddy non-tail-rec implementation of `apply`.
// For each U in T, define apply{U}, generic on every {'ret<U> for U in T}, taking a Cata and a U and returning a 'ret<U>.
// The body of apply{U} is given by matching on the cases of U.
module Cata =
let rec apply<'bret, 'ret> (cata : Cata<'bret, 'ret>) (e : Expr) : 'ret =
match e with
| Const c -> cata.Expr.Const c
| Pair (expr, expr1, pairOpKind) -> cata.Expr.Pair (apply cata expr) (apply cata expr1) pairOpKind
| Sequential exprs -> exprs |> List.map (apply cata) |> cata.Expr.Sequential
| Builder (expr, exprBuilder) -> cata.Expr.Builder (apply cata expr) (applyB cata exprBuilder)
and applyB<'bret, 'ret> (cata : Cata<'bret, 'ret>) (e : ExprBuilder) : 'bret =
match e with
| Child b -> cata.Builder.Child (applyB cata b)
| Parent p -> cata.Builder.Parent (apply cata p)
// The tail-recursive version is harder.
module TailRecCata =
[<RequireQualifiedAccess>]
type private Instruction =
| ProcessExpr of Expr
| ProcessBuilder of ExprBuilder
| Pair of PairOpKind
| Sequential of int
| Builder
| Child
| Parent
let private loop (cata : Cata<_, _>) (instructions : ResizeArray<_>) =
let resultsStack = ResizeArray ()
let builderResultsStack = ResizeArray ()
while instructions.Count > 0 do
let currentInstruction = instructions.[instructions.Count - 1]
instructions.RemoveAt (instructions.Count - 1)
match currentInstruction with
| Instruction.ProcessBuilder builder ->
match builder with
| Child exprBuilder ->
instructions.Add Instruction.Child
instructions.Add (Instruction.ProcessBuilder exprBuilder)
| Parent expr ->
instructions.Add Instruction.Parent
instructions.Add (Instruction.ProcessExpr expr)
| Instruction.ProcessExpr currentExpr ->
match currentExpr with
| Const c -> resultsStack.Add (cata.Expr.Const c)
| Pair (expr, expr1, pairOpKind) ->
instructions.Add (Instruction.Pair pairOpKind)
instructions.Add (Instruction.ProcessExpr expr1)
instructions.Add (Instruction.ProcessExpr expr)
| Sequential exprs ->
instructions.Add (Instruction.Sequential (List.length exprs))
for expr in exprs do
instructions.Add (Instruction.ProcessExpr expr)
| Builder (expr, exprBuilder) ->
instructions.Add Instruction.Builder
instructions.Add (Instruction.ProcessExpr expr)
instructions.Add (Instruction.ProcessBuilder exprBuilder)
| Instruction.Pair pairOpKind ->
let expr = resultsStack.[resultsStack.Count - 1]
let expr1 = resultsStack.[resultsStack.Count - 2]
resultsStack.RemoveRange (resultsStack.Count - 2, 2)
cata.Expr.Pair expr expr1 pairOpKind |> resultsStack.Add
| Instruction.Sequential count ->
let values =
seq {
for i = resultsStack.Count - 1 downto resultsStack.Count - count do
yield resultsStack.[i]
}
|> Seq.toList
resultsStack.RemoveRange (resultsStack.Count - count, count)
cata.Expr.Sequential values |> resultsStack.Add
| Instruction.Builder ->
let expr = resultsStack.[resultsStack.Count - 1]
resultsStack.RemoveAt (resultsStack.Count - 1)
let exprBuilder = builderResultsStack.[builderResultsStack.Count - 1]
builderResultsStack.RemoveAt (builderResultsStack.Count - 1)
cata.Expr.Builder expr exprBuilder |> resultsStack.Add
| Instruction.Child ->
let exprBuilder = builderResultsStack.[builderResultsStack.Count - 1]
builderResultsStack.RemoveAt (builderResultsStack.Count - 1)
cata.Builder.Child exprBuilder |> builderResultsStack.Add
| Instruction.Parent ->
let expr = resultsStack.[resultsStack.Count - 1]
resultsStack.RemoveAt (resultsStack.Count - 1)
cata.Builder.Parent expr |> builderResultsStack.Add
resultsStack, builderResultsStack
let run (cata : Cata<'bret, 'ret>) (e : Expr) : 'ret =
let instructions = ResizeArray ()
instructions.Add (Instruction.ProcessExpr e)
let resultsStack, builderResultsStack = loop cata instructions
if builderResultsStack.Count > 0 then
failwith "logic error"
Seq.exactlyOne resultsStack
let runBuilder (cata : Cata<'bret, 'ret>) (e : ExprBuilder) : 'bret =
let instructions = ResizeArray ()
instructions.Add (Instruction.ProcessBuilder e)
let resultsStack, builderResultsStack = loop cata instructions
if resultsStack.Count > 0 then
failwith "logic error"
Seq.exactlyOne builderResultsStack
module CataExample =
let id =
{
Expr =
{ new ExprCata<_, _> with
member _.Const x = Const x
member _.Pair x y z = Pair (x, y, z)
member _.Sequential xs = Sequential xs
member _.Builder x b = Builder (x, b)
}
Builder =
{ new ExprBuilderCata<_, _> with
member _.Child x = Child x
member _.Parent x = Parent x
}
}

View File

@@ -39,6 +39,10 @@
<Compile Include="GeneratedSerde.fs"> <Compile Include="GeneratedSerde.fs">
<MyriadFile>SerializationAndDeserialization.fs</MyriadFile> <MyriadFile>SerializationAndDeserialization.fs</MyriadFile>
</Compile> </Compile>
<Compile Include="Catamorphism.fs" />
<Compile Include="GeneratedCatamorphism.fs">
<MyriadFile>Catamorphism.fs</MyriadFile>
</Compile>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -0,0 +1,39 @@
//------------------------------------------------------------------------------
// This code was generated by myriad.
// Changes to this file will be lost when the code is regenerated.
//------------------------------------------------------------------------------
namespace ConsumePlugin
open WoofWare.Myriad.Plugins
/// Catamorphism
[<RequireQualifiedAccess>]
module ExprCata =
[<RequireQualifiedAccess>]
type private Instruction =
| ProcessExpr of Expr
| ProcessExprBuilder of ExprBuilder
| ExprPair of PairOpKind
| ExprSequential of int
| ExprBuilder
| ExprBuilderChild
| ExprBuilderParent
/// Execute the catamorphism.
let runExpr (cata : Cata<'ExprRet, 'ExprBuilderRet>) (x : Expr) : 'ExprRet =
let instructions = ResizeArray ()
instructions.Add (Instruction.ProcessExpr x)
let ExprRetStack, ExprBuilderRetStack = loop cata instructions
Seq.exactlyOne ExprRetStack
/// Execute the catamorphism.
let runExprBuilder (cata : Cata<'ExprRet, 'ExprBuilderRet>) (x : ExprBuilder) : 'ExprBuilderRet =
let instructions = ResizeArray ()
instructions.Add (Instruction.ProcessExprBuilder x)
let ExprRetStack, ExprBuilderRetStack = loop cata instructions
Seq.exactlyOne ExprBuilderRetStack

View File

@@ -62,3 +62,8 @@ type JsonParseAttribute (isExtensionMethod : bool) =
/// i.e. to stamp out HTTP REST clients from interfaces defining the API. /// i.e. to stamp out HTTP REST clients from interfaces defining the API.
type HttpClientAttribute () = type HttpClientAttribute () =
inherit Attribute () inherit Attribute ()
/// Attribute indicating a DU type to which the "create catamorphism" Myriad
/// generator should apply during build.
type CreateCatamorphismAttribute () =
inherit Attribute ()

View File

@@ -0,0 +1,403 @@
namespace WoofWare.Myriad.Plugins
open System
open System.Text
open Fantomas.FCS.Syntax
open Fantomas.FCS.SyntaxTrivia
open Fantomas.FCS.Xml
open Myriad.Core
[<RequireQualifiedAccess>]
module internal CataGenerator =
open Fantomas.FCS.Text.Range
open Myriad.Core.Ast
/// Returns a function:
/// let run{Case} (cata : Cata<{typars}>) (x : {Case}) : {TyPar} =
/// let instructions = ResizeArray ()
/// instructions.Add (Instruction.Process{Case} e)
/// let {typar1}Results, {typar2}Results, ... = loop cata instructions
/// { for all non-relevant typars: }
/// if {typar}Results.Count > 0 then failwith "logic error"
/// Seq.exactlyOne {relevantTypar}Stack
let createRunFunction (allTypars : SynType list) (relevantTypar : SynType) (unionType : SynTypeDefn) : SynBinding =
let relevantTypeName =
match unionType with
| SynTypeDefn.SynTypeDefn (SynComponentInfo.SynComponentInfo (longId = id), _, _, _, _, _) -> List.last id
let allTyparNames =
allTypars
|> List.map (fun ty ->
match ty with
| SynType.Var (SynTypar.SynTypar (ident = ident), _) -> ident
| _ -> failwith "logic error in generator"
)
let relevantTyparName =
match relevantTypar with
| SynType.Var (SynTypar.SynTypar (ident = ident), _) -> ident
| _ -> failwith "logic error in generator"
SynBinding.SynBinding (
None,
SynBindingKind.Normal,
false,
false,
[],
PreXmlDoc.Create " Execute the catamorphism.",
SynValData.SynValData (
None,
SynValInfo.SynValInfo (
[ [ SynArgInfo.CreateIdString "cata" ] ; [ SynArgInfo.CreateIdString "x" ] ],
SynArgInfo.SynArgInfo ([], false, None)
),
None
),
SynPat.CreateLongIdent (
SynLongIdent.CreateString ("run" + relevantTypeName.idText),
[
SynPat.CreateParen (
SynPat.CreateTyped (
SynPat.CreateNamed (Ident.Create "cata"),
SynType.App (
SynType.CreateLongIdent "Cata",
Some range0,
allTypars,
List.replicate (allTypars.Length - 1) range0,
Some range0,
false,
range0
)
)
)
SynPat.CreateParen (
SynPat.CreateTyped (
SynPat.CreateNamed (Ident.Create "x"),
SynType.CreateLongIdent (SynLongIdent.CreateFromLongIdent [ relevantTypeName ])
)
)
]
),
Some (SynBindingReturnInfo.Create relevantTypar),
SynExpr.CreateTyped (
SynExpr.LetOrUse (
false,
false,
[
SynBinding.Let (
valData = SynValData.SynValData (None, SynValInfo.Empty, None),
pattern = SynPat.CreateNamed (Ident.Create "instructions"),
expr =
SynExpr.CreateApp (
SynExpr.CreateIdentString "ResizeArray",
SynExpr.CreateConst SynConst.Unit
)
)
],
SynExpr.CreateSequential
[
SynExpr.CreateApp (
SynExpr.CreateLongIdent (SynLongIdent.Create [ "instructions" ; "Add" ]),
SynExpr.CreateParen (
SynExpr.CreateApp (
SynExpr.CreateLongIdent (
SynLongIdent.Create [ "Instruction" ; "Process" + relevantTypeName.idText ]
),
SynExpr.CreateLongIdent (SynLongIdent.CreateString "x")
)
)
)
SynExpr.LetOrUse (
false,
false,
[
SynBinding.Let (
valData = SynValData.SynValData (None, SynValInfo.Empty, None),
pattern =
SynPat.Tuple (
false,
List.map
(fun (t : Ident) ->
SynPat.CreateNamed (Ident.Create (t.idText + "Stack"))
)
allTyparNames,
List.replicate (allTypars.Length - 1) range0,
range0
),
expr =
SynExpr.CreateApp (
SynExpr.CreateApp (
SynExpr.CreateIdentString "loop",
SynExpr.CreateIdentString "cata"
),
SynExpr.CreateIdentString "instructions"
)
)
],
// TODO: add the "all other stacks are empty" sanity checks
SynExpr.CreateApp (
SynExpr.CreateLongIdent (SynLongIdent.Create [ "Seq" ; "exactlyOne" ]),
SynExpr.CreateIdent (Ident.Create (relevantTyparName.idText + "Stack"))
),
range0,
{
SynExprLetOrUseTrivia.InKeyword = None
}
)
],
range0,
{
InKeyword = None
}
),
relevantTypar
),
range0,
DebugPointAtBinding.NoneAtLet,
{
LeadingKeyword = SynLeadingKeyword.Let range0
InlineKeyword = None
EqualsRange = Some range0
}
)
let getName (ty : SynTypeDefn) : LongIdent =
match ty with
| SynTypeDefn.SynTypeDefn (SynComponentInfo.SynComponentInfo (_, _, _, id, _, _, _, _), _, _, _, _, _) -> id
type UnionField =
{
Type : SynType
Name : Ident option
}
type UnionCase =
{
Name : SynIdent
Fields : UnionField list
}
let getCases (SynTypeDefn.SynTypeDefn (_, repr, _, _, _, _)) : UnionCase list =
match repr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (_, cases, _), range0) ->
cases
|> List.map (fun (SynUnionCase.SynUnionCase (_, ident, kind, _, _, _, _)) ->
match kind with
| SynUnionCaseKind.FullType _ -> failwith "FullType union cases not supported"
| SynUnionCaseKind.Fields fields ->
{
Name = ident
Fields =
fields
|> List.map (fun (SynField.SynField (_, _, id, ty, _, _, _, _, _)) ->
{
Type = ty
Name = id
}
)
}
)
| _ -> failwithf "Failed to get union cases for type that was: %+A" repr
/// Given the input `| Pair of Expr * Expr * PairOpKind`,
/// strips out any members which contain recursive calls.
/// TODO: support lists and other compound types.
let createInstructionCases (allUnionTypes : SynTypeDefn list) (case : UnionCase) : UnionField list option =
let hasRecursion, cases =
((false, []), case.Fields)
||> List.fold (fun (hasRecursion, cases) field ->
match SynType.stripOptionalParen field.Type with
| ListType ty ->
match ty with
| SynType.LongIdent (SynLongIdent.SynLongIdent (ty, _, _)) ->
let isListOfSelf =
allUnionTypes
|> List.exists (fun unionTy -> List.last(getName unionTy).idText = List.last(ty).idText)
if isListOfSelf then
// store an int which is the length of the list
true, SynType.Int () :: cases
else
hasRecursion, field.Type :: cases
| _ -> hasRecursion, field.Type :: cases
| PrimitiveType _ -> hasRecursion, field.Type :: cases
| SynType.LongIdent (SynLongIdent.SynLongIdent (ty, _, _)) ->
let isSelf =
allUnionTypes
|> List.exists (fun unionTy -> List.last(getName unionTy).idText = List.last(ty).idText)
if isSelf then
true, cases
else
hasRecursion, field.Type :: cases
| _ -> failwithf "Unrecognised type: %+A" field.Type
)
if hasRecursion then
cases
|> List.rev
|> List.map (fun ty ->
{
Name = None
Type = ty
}
)
|> Some
else
None
let createInstructionType (allUnionTypes : SynTypeDefn list) : SynTypeDefn =
// One union case for each union type, and then
// a union case for each union case which contains a recursive reference.
let casesFromProcess : SynUnionCase list =
allUnionTypes
|> List.map (fun unionType ->
let name = getName unionType
SynUnionCase.Create (
Ident.Create ("Process" + (List.last name).idText),
[
SynField.Create (SynType.CreateLongIdent (SynLongIdent.CreateFromLongIdent name))
]
)
)
let casesFromCases =
allUnionTypes
|> List.collect (fun unionType ->
getCases unionType
|> List.choose (fun case ->
let fields = createInstructionCases allUnionTypes case
match fields with
| None -> None
| Some fields ->
let name =
match case.Name with
| SynIdent.SynIdent (ident, _) ->
(List.last (getName unionType)).idText + ident.idText |> Ident.Create
SynUnionCase.Create (name, fields |> List.map (fun field -> SynField.Create field.Type))
|> Some
)
)
let cases = casesFromProcess @ casesFromCases
SynTypeDefn.SynTypeDefn (
SynComponentInfo.SynComponentInfo (
[ SynAttributeList.Create [ SynAttribute.RequireQualifiedAccess () ] ],
None,
[],
[ Ident.Create "Instruction" ],
PreXmlDoc.Empty,
false,
Some (SynAccess.Private range0),
range0
),
SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union (None, cases, range0), range0),
[],
None,
range0,
{
LeadingKeyword = SynTypeDefnLeadingKeyword.Type range0
EqualsRange = Some range0
WithKeyword = None
}
)
let createModule
(opens : SynOpenDeclTarget list)
(ns : LongIdent)
(taggedType : SynTypeDefn)
(allUnionTypes : SynTypeDefn list)
: SynModuleOrNamespace
=
let moduleName : LongIdent =
List.last (getName taggedType)
|> fun x -> x.idText + "Cata"
|> Ident.Create
|> List.singleton
let attribs = [ SynAttributeList.Create (SynAttribute.RequireQualifiedAccess ()) ]
let modInfo =
SynComponentInfo.Create (
moduleName,
attributes = attribs,
xmldoc = PreXmlDoc.Create " Catamorphism" // TODO: better docstring
)
let allTypars =
allUnionTypes
|> List.map (fun unionType ->
List.last (getName unionType)
|> fun x -> x.idText
|> fun s -> s + "Ret"
|> Ident.Create
|> fun x -> SynTypar.SynTypar (x, TyparStaticReq.None, false)
|> fun x -> SynType.Var (x, range0)
)
let runFunctions =
List.zip allUnionTypes allTypars
|> List.map (fun (unionType, relevantTypar) -> createRunFunction allTypars relevantTypar unionType)
SynModuleOrNamespace.CreateNamespace (
ns,
decls =
[
for openStatement in opens do
yield SynModuleDecl.CreateOpen openStatement
yield
SynModuleDecl.CreateNestedModule (
modInfo,
[
SynModuleDecl.Types ([ createInstructionType allUnionTypes ], range0)
SynModuleDecl.CreateLet runFunctions
]
)
]
)
/// Myriad generator that provides an HTTP client for an interface type using RestEase annotations.
[<MyriadGenerator("create-catamorphism")>]
type CreateCatamorphismGenerator () =
interface IMyriadGenerator with
member _.ValidInputExtensions = [ ".fs" ]
member _.Generate (context : GeneratorContext) =
let ast, _ =
Ast.fromFilename context.InputFilename |> Async.RunSynchronously |> Array.head
let types = Ast.extractTypeDefn ast
let opens = AstHelper.extractOpens ast
let namespaceAndTypes =
types
|> List.choose (fun (ns, types) ->
match types |> List.tryFind Ast.hasAttribute<CreateCatamorphismAttribute> with
| Some taggedType ->
let anyNonUnion =
types
|> List.exists (fun (SynTypeDefn.SynTypeDefn (_, repr, _, _, _, _)) ->
match repr with
| SynTypeDefnRepr.Simple (SynTypeDefnSimpleRepr.Union _, _) -> false
| _ -> true
)
if anyNonUnion then
failwith
"Error: all types recursively defined together with a CreateCatamorphism type must be discriminated unions"
Some (ns, taggedType, types)
| _ -> None
)
let modules =
namespaceAndTypes
|> List.map (fun (ns, taggedType, types) -> CataGenerator.createModule opens ns taggedType types)
Output.Ast modules

View File

@@ -34,6 +34,7 @@
<Compile Include="JsonSerializeGenerator.fs"/> <Compile Include="JsonSerializeGenerator.fs"/>
<Compile Include="JsonParseGenerator.fs"/> <Compile Include="JsonParseGenerator.fs"/>
<Compile Include="HttpClientGenerator.fs"/> <Compile Include="HttpClientGenerator.fs"/>
<Compile Include="CataGenerator.fs" />
<EmbeddedResource Include="version.json"/> <EmbeddedResource Include="version.json"/>
<EmbeddedResource Include="SurfaceBaseline.txt"/> <EmbeddedResource Include="SurfaceBaseline.txt"/>
<None Include="..\README.md"> <None Include="..\README.md">