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

PureScript API code generation #146

Draft
wants to merge 20 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Dockerfile
.dockerignore
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,4 @@ scribble-core/src/test/scrib/test/test9/
bin/scribblec.sh
permissions-fix.sh

.idea
17 changes: 17 additions & 0 deletions Arithmetic.scr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Arithmetic;

type <purescript> "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;
}
}
11 changes: 11 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
64 changes: 64 additions & 0 deletions MathServer.purs
Original file line number Diff line number Diff line change
@@ -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 <purescript> "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
1 change: 1 addition & 0 deletions scribble-cli/src/main/java/org/scribble/cli/CLArgFlag.java
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public enum CLArgFlag
SGRAPH_PNG,
UNFAIR_SGRAPH_PNG,
API_GEN,
API_GEN_PS,
SESS_API_GEN,
SCHAN_API_GEN,
}
5 changes: 4 additions & 1 deletion scribble-cli/src/main/java/org/scribble/cli/CLArgParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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];
Expand Down
18 changes: 18 additions & 0 deletions scribble-cli/src/main/java/org/scribble/cli/CommandLine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<String, String> classes = psgen.generateApi(fullname);
outputClasses(classes);
}
}

private void outputSessionApi(Job job) throws ScribbleException, CommandLineException
{
JobContext jcontext = job.getContext();
Expand Down
Original file line number Diff line number Diff line change
@@ -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<ForeignType> 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<ForeignType> 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();
}
}
}
Original file line number Diff line number Diff line change
@@ -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<ForeignType> types) {
StringBuilder is = new StringBuilder();
HashMap<String, Set<String>> imports = new HashMap();
for (ForeignType type : types) {
if (imports.containsKey(type.source)) {
imports.get(type.source).add(type.name);
} else {
Set<String> 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<String> 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);
}
}