diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..4a246ec6c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +.dockerignore diff --git a/.gitignore b/.gitignore index c6aa4d053..bef513c3a 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ scribble-core/src/test/scrib/test/test9/ bin/scribblec.sh permissions-fix.sh +.idea diff --git a/Arithmetic.scr b/Arithmetic.scr new file mode 100644 index 000000000..eff1657ad --- /dev/null +++ b/Arithmetic.scr @@ -0,0 +1,17 @@ +module Arithmetic; + +type "Int" from "Prim" as Int; + +global protocol MathServer(role Client, role Server) { + choice at Client { + Add(Int, Int) from Client to Server; + Sum(Int) from Server to Client; + do MathServer(Client, Server); + } or { + Multiply(Int, Int) from Client to Server; + Product(Int) from Server to Client; + do MathServer(Client, Server); + } or { + Quit() from Client to Server; + } +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..2a1001e5c --- /dev/null +++ b/Dockerfile @@ -0,0 +1,11 @@ +FROM maven:3.6.3-jdk-14 +RUN yum install -y unzip +COPY . /scribble-java +RUN cd scribble-java \ + && mvn install -Dlicense.skip=true \ + && mv scribble-dist/target/scribble-dist* / + +RUN unzip scribble-dist* +RUN chmod 755 scribblec.sh + +ENTRYPOINT ["./scribblec.sh"] diff --git a/MathServer.purs b/MathServer.purs new file mode 100644 index 000000000..798c00969 --- /dev/null +++ b/MathServer.purs @@ -0,0 +1,64 @@ +-- module Scribble.Protocol.{module name}.{protocol name} where +module Scribble.Protocol.Arithmetic.MathServer where + +-- Hard coded import list +import Scribble.FSM (class Branch, class Initial, class Terminal, class Receive, class Select, class Send, kind Role) +import Type.Row (Cons, Nil) +import Data.Void (Void) + +-- Type declaration imports +-- e.g. type "Int" from "Prim" as Int; +import Prim (Int) + +-- Message data types +-- TODO: Derive JSON encodings for them! +data Add = Add Int Int +data Sum = Sum Int +data Multiply = Add Int Int +data Product = Product Int +data Quit = Quit + +-- Client role definition +foreign import data Client :: Role + +-- Client states +foreign import data S6 :: Type +foreign import data S6Add :: Type +foreign import data S6Quit :: Type +foreign import data S6Multiply :: Type +foreign import data S7 :: Type +foreign import data S8 :: Type +foreign import data S9 :: Type + +instance initialClient :: Initial Client S6 +instance terminalClient :: Terminal Client S7 + +-- Client state transitions +-- Branch names are the lowercase of the message label expected to receive +instance selectS6 :: Select Server S6 (Cons "quit" S6Quit (Cons "add" S6Add (Cons "multiply" S6Multiply Nil))) +instance sendS6Quit :: Send Server S6Quit S7 Quit +instance sendS6Add :: Send Server S6Add S8 Add +instance sendS6Multiply :: Send Server S6Multiply S9 Multiply +instance receiveS8 :: Receive Server S8 S6 Sum +instance receiveS9 :: Receive Server S9 S6 Product + +foreign import data Server :: Role + +foreign import data S16 :: Type +foreign import data S16Add :: Type +foreign import data S16Quit :: Type +foreign import data S16Multiply :: Type +foreign import data S17 :: Type +foreign import data S18 :: Type +foreign import data S19 :: Type + +instance initialServer :: Initial Server S16 +instance terminalServer :: Terminal Server S17 + +-- This isn't a typo, it should be Branch Server (see the typeclass for more details) +instance branchS16 :: Branch Server S16 (Cons "quit" S16Quit (Cons "add" S16Add (Cons "multiply" S16Multiply Nil))) +instance receiveS16Quit :: Receive Client S16Quit S17 Quit +instance receiveS16Add :: Receive Client S16Add S18 Add +instance receiveS16Multiply :: Receive Client S16Multiply S19 Multiply +instance sendS8 :: Send Server S18 S16 Sum +instance sendS9 :: Send Server S19 S16 Product diff --git a/scribble-cli/src/main/java/org/scribble/cli/CLArgFlag.java b/scribble-cli/src/main/java/org/scribble/cli/CLArgFlag.java index e14cf7ded..a251a528c 100644 --- a/scribble-cli/src/main/java/org/scribble/cli/CLArgFlag.java +++ b/scribble-cli/src/main/java/org/scribble/cli/CLArgFlag.java @@ -46,6 +46,7 @@ public enum CLArgFlag SGRAPH_PNG, UNFAIR_SGRAPH_PNG, API_GEN, + API_GEN_PS, SESS_API_GEN, SCHAN_API_GEN, } diff --git a/scribble-cli/src/main/java/org/scribble/cli/CLArgParser.java b/scribble-cli/src/main/java/org/scribble/cli/CLArgParser.java index e14bd49db..e3d02f7eb 100644 --- a/scribble-cli/src/main/java/org/scribble/cli/CLArgParser.java +++ b/scribble-cli/src/main/java/org/scribble/cli/CLArgParser.java @@ -51,6 +51,7 @@ public class CLArgParser public static final String SGRAPH_PNG_FLAG = "-modelpng"; public static final String UNFAIR_SGRAPH_PNG_FLAG = "-umodelpng"; public static final String API_GEN_FLAG = "-api"; + public static final String API_GEN_PS_FLAG = "-api-ps"; public static final String SESSION_API_GEN_FLAG = "-sessapi"; public static final String STATECHAN_API_GEN_FLAG = "-chanapi"; @@ -86,6 +87,7 @@ public class CLArgParser CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.SGRAPH_PNG_FLAG, CLArgFlag.SGRAPH_PNG); CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.UNFAIR_SGRAPH_PNG_FLAG, CLArgFlag.UNFAIR_SGRAPH_PNG); CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.API_GEN_FLAG, CLArgFlag.API_GEN); + CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.API_GEN_PS_FLAG, CLArgFlag.API_GEN_PS); CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.SESSION_API_GEN_FLAG, CLArgFlag.SESS_API_GEN); CLArgParser.NON_UNIQUE_FLAGS.put(CLArgParser.STATECHAN_API_GEN_FLAG, CLArgFlag.SCHAN_API_GEN); } @@ -238,6 +240,7 @@ protected int parseFlag(int i) throws CommandLineException case CLArgParser.SGRAPH_FLAG: case CLArgParser.UNFAIR_SGRAPH_FLAG: case CLArgParser.SESSION_API_GEN_FLAG: + case CLArgParser.API_GEN_PS_FLAG: { return parseProtoArg(flag, i); } @@ -322,7 +325,7 @@ private int parseProject(int i) throws CommandLineException // Similar to parse { if ((i + 2) >= this.args.length) { - throw new CommandLineException("Missing protocol/role arguments"); +// throw new CommandLineException("Missing protocol/role arguments"); } String proto = this.args[++i]; String role = this.args[++i]; diff --git a/scribble-cli/src/main/java/org/scribble/cli/CommandLine.java b/scribble-cli/src/main/java/org/scribble/cli/CommandLine.java index 20f9ebfca..b6ee490a6 100644 --- a/scribble-cli/src/main/java/org/scribble/cli/CommandLine.java +++ b/scribble-cli/src/main/java/org/scribble/cli/CommandLine.java @@ -27,6 +27,7 @@ import org.scribble.ast.ProtocolDecl; import org.scribble.ast.global.GProtocolDecl; import org.scribble.codegen.java.JEndpointApiGenerator; +import org.scribble.codegen.purescript.PSEndpointApiGenerator; import org.scribble.main.AntlrSourceException; import org.scribble.main.Job; import org.scribble.main.JobContext; @@ -261,6 +262,10 @@ protected void doNonAttemptableOutputTasks(Job job) throws ScribbleException, Co { outputEndpointApi(job); } + if (this.args.containsKey(CLArgFlag.API_GEN_PS)) + { + outputEndpointApiPureScript(job); + } } // FIXME: option to write to file, like classes @@ -390,6 +395,19 @@ private void outputEndpointApi(Job job) throws ScribbleException, CommandLineExc } } + private void outputEndpointApiPureScript(Job job) throws ScribbleException, CommandLineException + { + JobContext jcontext = job.getContext(); + String[] args = this.args.get(CLArgFlag.API_GEN_PS); + PSEndpointApiGenerator psgen = new PSEndpointApiGenerator(job); + for (int i = 0; i < args.length; i += 2) + { + GProtocolName fullname = checkGlobalProtocolArg(jcontext, args[i]); + Map classes = psgen.generateApi(fullname); + outputClasses(classes); + } + } + private void outputSessionApi(Job job) throws ScribbleException, CommandLineException { JobContext jcontext = job.getContext(); diff --git a/scribble-codegen/src/main/java/org/scribble/codegen/purescript/DataType.java b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/DataType.java new file mode 100644 index 000000000..5215f4f3a --- /dev/null +++ b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/DataType.java @@ -0,0 +1,68 @@ +package org.scribble.codegen.purescript; + +import java.util.List; +import java.util.Objects; + +public class DataType { + public static final String KIND_TYPE = "Type"; + public final String name; + private final List params; + private final String kind; + private boolean isForeign; + + public static boolean isValidName(String name) { + // Names must begin with capital letters and contain only letters and digits + return !(name.isEmpty() || name.charAt(0) < 65 || name.charAt(0) > 90); + } + + public DataType(String name, List args, String kind, boolean isForeign) { + this.isForeign = isForeign; + if (!isValidName(name)) { + throw new RuntimeException("`" + name + "' is an invalid data type name"); + } + if (!isValidName(kind)) { + throw new RuntimeException("`" + kind + "' is an invalid data type kind"); + } + this.name = name; + this.params = args; + this.kind = kind; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DataType dataType = (DataType) o; + return Objects.equals(params, dataType.params) && + Objects.equals(name, dataType.name) && + Objects.equals(kind, dataType.kind); + } + + @Override + public int hashCode() { + return Objects.hash(params, name, kind); + } + + public String generateDataType() { + if (isForeign) { + return "foreign import data " + name + " :: " + kind + "\n"; + } else { + // TODO: Potential newtype optimisation for constructors with exactly one value + // TODO: Derive JSON encoding/decoding + StringBuilder sb = new StringBuilder(); + sb.append("data " + name + " = " + name); + for (ForeignType type : params) { + sb.append(" " + type.name); + } + sb.append("\n"); + if (kind.equals(KIND_TYPE)) { + sb.append("derive instance generic" + name + " :: Generic " + name + " _\n"); + sb.append("instance encodeJson" + name + " :: EncodeJson " + name + " where\n"); + sb.append(" encodeJson = genericEncodeJsonWith jsonEncoding\n"); + sb.append("instance decodeJson" + name + " :: DecodeJson " + name + " where\n"); + sb.append(" decodeJson = genericDecodeJsonWith jsonEncoding\n"); + } + return sb.toString(); + } + } +} diff --git a/scribble-codegen/src/main/java/org/scribble/codegen/purescript/ForeignType.java b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/ForeignType.java new file mode 100644 index 000000000..8f2ec7331 --- /dev/null +++ b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/ForeignType.java @@ -0,0 +1,64 @@ +package org.scribble.codegen.purescript; + +import org.scribble.main.RuntimeScribbleException; + +import java.util.*; + +public class ForeignType { + public static final String PRIM_MODULE = "Prim"; + public static final String[] PRIMS = new String[]{"Number", "Int", "String", "Char", "Boolean"}; + public final String name; + public final String source; + // Primitive types in the language - no need to explicitly import + // See https://pursuit.purescript.org/builtins/docs/Prim + + public ForeignType(String name, String source) { + this.name = name; + this.source = source; + } + + // Group by package source + public static String generateImports(Set types) { + StringBuilder is = new StringBuilder(); + HashMap> imports = new HashMap(); + for (ForeignType type : types) { + if (imports.containsKey(type.source)) { + imports.get(type.source).add(type.name); + } else { + Set mod = new HashSet<>(); + mod.add(type.name); + imports.put(type.source, mod); + } + } + for (String source : imports.keySet()) { + // No need to explicitly import + if (source.equals(PRIM_MODULE)) continue; + if (source.trim().length() == 0) { + throw new RuntimeScribbleException("Foreign type import source for " + imports.get(source) + " cannot be empty"); + } + is.append("import " + source + " ("); + List ts = new ArrayList<>(imports.get(source)); + is.append(ts.get(0)); + ts.remove(0); + for (String type : ts) { + is.append(", " + type); + } + is.append(")\n"); + } + return is.toString(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ForeignType that = (ForeignType) o; + return Objects.equals(name, that.name) && + Objects.equals(source, that.source); + } + + @Override + public int hashCode() { + return Objects.hash(name, source); + } +} diff --git a/scribble-codegen/src/main/java/org/scribble/codegen/purescript/PSEndpointApiGenerator.java b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/PSEndpointApiGenerator.java new file mode 100644 index 000000000..104eaaeb3 --- /dev/null +++ b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/PSEndpointApiGenerator.java @@ -0,0 +1,367 @@ +/** + * Copyright 2008 The Scribble Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License + * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express + * or implied. See the License for the specific language governing permissions and limitations under + * the License. + */ +package org.scribble.codegen.purescript; + +import java.util.*; + +import org.scribble.ast.Module; +import org.scribble.ast.NonProtocolDecl; +import org.scribble.ast.global.GProtocolDecl; +import org.scribble.main.Job; +import org.scribble.main.JobContext; +import org.scribble.main.RuntimeScribbleException; +import org.scribble.main.ScribbleException; +import org.scribble.model.endpoint.EGraph; +import org.scribble.model.endpoint.EState; +import org.scribble.model.endpoint.actions.EAction; +import org.scribble.type.Payload; +import org.scribble.type.kind.PayloadTypeKind; +import org.scribble.type.name.GProtocolName; +import org.scribble.type.name.PayloadElemType; +import org.scribble.type.name.Role; +import org.scribble.util.Pair; + +import static java.util.stream.Collectors.joining; + +public class PSEndpointApiGenerator +{ + public static final String PURESCRIPT_SCHEMA = "purescript"; + public final Job job; + + public PSEndpointApiGenerator(Job job) + { + this.job = job; + } + + public Map generateApi(GProtocolName fullname) throws ScribbleException { + this.job.debugPrintln("\n[Purescript API gen] Running for " + fullname); + + Module mod = this.job.getContext().getModule(fullname.getPrefix()); + GProtocolName simpname = fullname.getSimpleName(); + GProtocolDecl gpd = (GProtocolDecl) mod.getProtocolDecl(simpname); + + if (!gpd.isExplicitModifier()) { + throw new RuntimeScribbleException("Only protocols with explicit connections are currently supported in this version"); + } + + String moduleName = fullname.getPrefix().toString(); + String protocolName = fullname.getSimpleName().toString(); + + // Generate protocol type-level information + DataType protocolType = new DataType(protocolName, null, "Protocol", true); +// TypeClassInstance protocolNameInst = new TypeClassInstance("protocolName" + protocolType.name, "ProtocolName", new String[] {protocolType.name, ("\"" + protocolType.name + "\"")}); + +// StringBuilder roleNames = new StringBuilder("("); +// // For each role make a projection, then traverse the graph getting the states + transitions +// for (Role r : gpd.header.roledecls.getRoles()) { +// roleNames.append("\"" + r.toString() + "\" ::: "); +// } +// roleNames.append("SNil)"); +// TypeClassInstance protocolRoleNames = new TypeClassInstance("protocolRoleNames" + protocolType.name, "ProtocolRoleNames", new String[] {protocolType.name, roleNames.toString()}); + + // Message actions and their corresponding datatype + Map datatypes = new HashMap<>(); + + Map foreignImports = new HashMap<>(); + // Get the foreign type declarations + // TODO: Only include imports that are used + // TODO: Make it clear that JSON encoding/decoding instances are required + for (NonProtocolDecl foreignType : mod.getNonProtocolDecls()) { + if (!foreignType.schema.equals(PURESCRIPT_SCHEMA)) { + throw new ScribbleException(foreignType.getSource(), "Unsupported data type schema: " + foreignType.schema); + } + foreignImports.put(foreignType.name.toString(), new ForeignType(foreignType.extName, foreignType.extSource)); + } + + Map, List>> efsms = new HashMap(); + + // For each role make a projection, then traverse the graph getting the states + transitions + for (Role r : gpd.header.roledecls.getRoles()) { + JobContext jc = job.getContext(); + EGraph efsm = job.minEfsm ? jc.getMinimisedEGraph(fullname, r) : jc.getEGraph(fullname, r); + + EState init = efsm.init; + EState term = EState.getTerminal(init); + + DataType role = new DataType(r.toString(), null, "Role", true); + + List instances = new ArrayList<>(); + List states = new ArrayList<>(); + + // Add the instances for initial/terminal nodes + instances.add(new TypeClassInstance(("initial" + role.name), "Initial", new String[] {role.name, getStateTypeName(init)})); + String t = term == null ? "Void" : getStateTypeName(term); + instances.add(new TypeClassInstance(("terminal" + role.name), "Terminal", new String[] {role.name, t})); + + Set seen = new HashSet<>(); + Set level = new HashSet<>(); + + // Perform a breadth first traversal over the graph + level.add(init); + while (!level.isEmpty()) { + Set nextlevel = new HashSet<>(); + for (EState s : level) { + // Don't generate more than once + if (seen.contains(s.toString())) { + continue; + } + seen.add(s.toString()); + // View the successors during the next level + nextlevel.addAll(s.getAllSuccessors()); + + // Generate the state type + String curr = getStateTypeName(s); + states.add(new DataType(curr, null, DataType.KIND_TYPE, true)); + switch (s.getStateKind()) { + case OUTPUT: { + if (s.getAllActions().size() == 1) { + String next = getStateTypeName(s.getAllSuccessors().get(0)); + EAction action = s.getAllActions().get(0); + String to = action.obj.toString(); + if (action.isSend()) { + String type = action.mid.toString(); + // Add the instance and message data type + instances.add(new TypeClassInstance(("send" + curr), "Send", new String[]{to, curr, next, type})); + addDatatype(datatypes, action, foreignImports); + } else if (action.isDisconnect()) { + instances.add(new TypeClassInstance(("disconnect" + curr), "Disconnect", new String[]{r.toString(), to, curr, next})); + + } else if (action.isRequest()) { + String connectedState = curr + "Connected"; + instances.add(new TypeClassInstance(("connect" + curr), "Connect", new String[]{r.toString(), to, curr, connectedState})); + String type = action.mid.toString(); + // Add the instance and message data type + states.add(new DataType(curr + "Connected", null, DataType.KIND_TYPE, true)); + instances.add(new TypeClassInstance(("send" + curr), "Send", new String[]{to, connectedState, next, type})); + addDatatype(datatypes, action, foreignImports); + } else { + // TODO: What is wrap-client + do we need to handle it? + throw new ScribbleException(null, "Unsupported action " + s.getStateKind()); + } + } else { + // If there are multiple output actions, we should treat it as a branch and create some dummy states where we send the label + // TODO: I'm making the assumption that if there are multiple outputs, they are all send -- probably should test this + Set> choices = new HashSet<>(); + for (int i = 0; i < s.getAllActions().size(); i++) { + EAction action = s.getAllActions().get(i); + String next = getStateTypeName(s.getAllSuccessors().get(i)); + String labelState = getStateTypeName(s) + action.mid; + String label = action.mid.toString().toLowerCase(); + String type = action.mid.toString(); + String to = action.obj.toString(); + choices.add(new Pair(label, labelState)); + + // Add the instance and message data type and dummy state + states.add(new DataType(labelState, null, DataType.KIND_TYPE, true)); + instances.add(new TypeClassInstance("send" + labelState, "Send", new String[] {to, labelState, next, type})); + addDatatype(datatypes, action, foreignImports); + } + + // All messages must be to the same role, so we can pick the first one + EAction action = s.getAllActions().get(0); + String to = action.obj.toString(); + + String branches = choices.stream() + .map(option -> "\"" + option.left + "\" :: " + option.right) + .collect(joining(", ", "(", ")")); + + instances.add(new TypeClassInstance("select" + curr, "Select", new String[] {to, curr, branches})); + } + } + break; + case UNARY_INPUT: { + String next = getStateTypeName(s.getAllSuccessors().get(0)); + EAction action = s.getAllActions().get(0); + String type = action.mid.toString(); + String from = action.obj.toString(); + + // Add the instance and message data type + instances.add(new TypeClassInstance("receive" + curr, "Receive", new String[] {from, curr, next, type})); + addDatatype(datatypes, action, foreignImports); + } + break; + case POLY_INPUT: { + Set> choices = new HashSet<>(); + for (int i = 0; i < s.getAllActions().size(); i++) { + EAction action = s.getAllActions().get(i); + String next = getStateTypeName(s.getAllSuccessors().get(i)); + String labelState = getStateTypeName(s) + action.mid; + String label = action.mid.toString().toLowerCase(); + String type = action.mid.toString(); + String to = action.obj.toString(); + choices.add(new Pair(label, labelState)); + + // Add the instance and message data type and dummy state + states.add(new DataType(labelState, null, DataType.KIND_TYPE, true)); + instances.add(new TypeClassInstance("receive" + labelState, "Receive", new String[] {to, labelState, next, type})); + addDatatype(datatypes, action, foreignImports); + } + + // All messages must be to the same role, so we can pick the first one + String choosing = s.getAllActions().get(0).obj.toString(); + + String branches = choices.stream() + .map(option -> "\"" + option.left + "\" :: " + option.right) + .collect(joining(", ", "(", ")")); + + instances.add(new TypeClassInstance("branch" + curr, "Branch", new String[] {role.name, choosing, curr, branches})); + } + break; + case TERMINAL: + break; + case ACCEPT: + EAction action = s.getAllActions().get(0); + String next = getStateTypeName(s.getAllSuccessors().get(0)); + String to = action.obj.toString(); + instances.add(new TypeClassInstance(("accept" + curr), "Accept", new String[]{r.toString(), to, curr, next})); + break; + case WRAP_SERVER: + throw new RuntimeScribbleException("Unsupported action " + s.getStateKind()); + } + + } + + + level = nextlevel; + } + + efsms.put(role, new Pair<>(states, instances)); + } + + // Perform the code generation + List sections = new ArrayList<>(); + sections.add(moduleDeclaration(moduleName, protocolName)); + sections.add(staticImports()); + + sections.add(jsonImports()); + + // Foreign imports + sections.add(ForeignType.generateImports(new HashSet<>(foreignImports.values()))); + + // TODO: Maybe this should be refactored out? + sections.add(jsonEncoding()); + + // Message data types + StringBuilder dt = new StringBuilder(); + for (String type : datatypes.keySet()) { + List arguments = new ArrayList<>(); + for (PayloadElemType elem : datatypes.get(type).elems) { + arguments.add(foreignImports.get(elem.toString())); + } + dt.append(new DataType(type, arguments, DataType.KIND_TYPE, false).generateDataType()); + } + sections.add(dt.toString()); + + + // Protocol + sections.add(protocolType.generateDataType()); +// sections.add(protocolNameInst.generateInstance()); +// sections.add(protocolRoleNames.generateInstance()); + + // ProtocolRoleNames + + // EFSMs + for (DataType role : efsms.keySet()) { + sections.add(role.generateDataType()); + TypeClassInstance roleName = new TypeClassInstance("roleName" + role.name, "RoleName", new String[] {role.name, ("\"" + role.name + "\"")}); + sections.add(roleName.generateInstance()); + + StringBuilder states = new StringBuilder(); + for (DataType state : efsms.get(role).left) { + states.append(state.generateDataType()); + } + sections.add(states.toString()); + + StringBuilder instances = new StringBuilder(); + for (TypeClassInstance instance : efsms.get(role).right) { + instances.append(instance.generateInstance()); + } + sections.add(instances.toString()); + } + + // Add newlines between sections + StringBuilder module = new StringBuilder(); + for (String section : sections) { + module.append(section); + module.append("\n"); + } + + Map map = new HashMap(); + map.put(makePath(moduleName, protocolName), module.toString()); + + return map; + } + + private static String moduleDeclaration(String moduleName, String protocolName) { + return "module Scribble.Protocol." + moduleName + "." + protocolName + " where\n"; + } + + private static String staticImports() { + StringBuilder sb = new StringBuilder(); + sb.append("import Scribble.FSM\n"); +// sb.append("import Scribble.Type.SList (type (:::), SLProxy(..), SNil, symbols)\n"); + sb.append("import Data.Void (Void)\n"); + sb.append("import Data.Tuple (Tuple)\n"); + return sb.toString(); + } + + private static String jsonImports() { + StringBuilder sb = new StringBuilder(); + sb.append("-- From purescript-argonaut-codecs\n"); + sb.append("import Data.Argonaut.Decode (class DecodeJson)\n"); + sb.append("import Data.Argonaut.Encode (class EncodeJson)\n"); + sb.append("import Data.Argonaut.Core (Json) -- From purescript-argonaut-core\n"); + sb.append("import Data.Generic.Rep (class Generic) -- From purescript-generics-rep\n"); + sb.append("-- From purescript-argonaut-generic\n"); + sb.append("import Data.Argonaut.Decode.Generic.Rep (genericDecodeJsonWith)\n"); + sb.append("import Data.Argonaut.Encode.Generic.Rep (genericEncodeJsonWith)\n"); + sb.append("import Data.Argonaut.Types.Generic.Rep (Encoding)\n"); + + return sb.toString(); + } + + private static String jsonEncoding() { + StringBuilder sb = new StringBuilder(); + sb.append("jsonEncoding :: Encoding\n"); + sb.append("jsonEncoding =\n"); + sb.append(" { tagKey: \"tag\"\n"); + sb.append(" , valuesKey: \"values\"\n"); + sb.append(" , unwrapSingleArguments: true\n"); + sb.append(" }\n"); + + return sb.toString(); + } + + private void addDatatype(Map datatypes, EAction action, Map foreignImports) throws RuntimeException { + String datatype = action.mid.toString(); + if (datatypes.containsKey(datatype)) { + if (!datatypes.get(datatype).equals(action.payload)) { + throw new RuntimeException("Messages with the same name `" + action.mid + "` must have the same payload"); + } + } else { + datatypes.put(datatype, action.payload); + } + } + + private static String makePath(String module, String protocol) + { + return "Scribble/Protocol/" + module.replace('.', '/') + "/" + protocol + ".purs"; + } + + public static String getStateTypeName(EState s) + { + return "S" + s; + } + +} diff --git a/scribble-codegen/src/main/java/org/scribble/codegen/purescript/TypeClassInstance.java b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/TypeClassInstance.java new file mode 100644 index 000000000..d80b0112f --- /dev/null +++ b/scribble-codegen/src/main/java/org/scribble/codegen/purescript/TypeClassInstance.java @@ -0,0 +1,25 @@ +package org.scribble.codegen.purescript; + +import java.util.List; + +public class TypeClassInstance { + private final String instance; + private final String typeclass; + private final String[] parameters; + + public TypeClassInstance(String instance, String typeclass, String[] parameters) { + this.instance = instance; + this.typeclass = typeclass; + this.parameters = parameters; + } + + public String generateInstance() { + StringBuilder sb = new StringBuilder(); + sb.append("instance " + instance + " :: " + typeclass); + for (String parameter : parameters) { + sb.append(" " + parameter); + } + sb.append("\n"); + return sb.toString(); + } +} diff --git a/scribble-dist/src/main/resources/scribblec.sh b/scribble-dist/src/main/resources/scribblec.sh old mode 100644 new mode 100755 index 5f2df50b3..f5ff96e23 --- a/scribble-dist/src/main/resources/scribblec.sh +++ b/scribble-dist/src/main/resources/scribblec.sh @@ -116,6 +116,8 @@ CLASSPATH=$CLASSPATH':'$DIR'/'$LIB'/scribble-codegen.jar' CLASSPATH=$CLASSPATH':'$DIR'/'$LIB'/stringtemplate.jar' CLASSPATH="'"`fixpath "$CLASSPATH"`"'" +echo $DIR + usage=0 verbose=0 dot=0