diff --git a/README.md b/README.md index bf44f90..3f50c3b 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,19 @@ -##The Care and Feeding of -#SMC -##The State Machine Compiler +## The Care and Feeding of +# SMC +## The State Machine Compiler -SMC is a Java application that translates a state transition table into a program that implements the described state machine. Output languages include Java, C, and C++. Adding other languages is trivial. +SMC is a Java application that translates a state transition table into a program that implements the described state machine. Output languages include Java, Go, C, and C++. Adding other languages is trivial. + +### Command Line +`ant compile && ant jar` -###Command Line `java -jar smc.jar -l -o -f ` - * `` is either `C`, `Cpp`, or `Java`. + * `` is one of: `C`, `Cpp`, `Go`, or `Java`. * `` is the output directory. Your new state machine will be written there. * `` currently for Java only. `package:package_name` will put the appropriate `package` statement in the generated code. -###Syntax +### Syntax The syntax for the state transition table is based on a simple state transition table. Here is a straightforward example that describes the logic of a subway turnstile. `turnstile.sm`: Initial: Locked @@ -30,7 +32,7 @@ When this is run through SMC it produces the source code for a state machine nam * Given we are in the `Unlocked` state, when we get a `Coin` event, then we stay in the `Unlocked` state and invoke the `thankyou` action. * GIven we are in the `Unlocked` state, when we get a `Pass` event, then we transition to the `Locked` state and invoke the `lock` action. -###Opacity +### Opacity One of the goals of SMC is to produce code that the programmer never needs to look at, and does not check in to source code control. It is intended that SMC will generate the appropriate code during the pre-compile phase of your build. The output of SMC is two sets of functions: The _Event_ functions and the _Actions_ functions. For most languages these functions will be arranged into an abstract class in which the _Event_ functions are public, and the _Action_ functions are protected and abstract. @@ -196,7 +198,7 @@ We use the _dash_ (`-`) character for two purposes. When used as an action it m When more than one action should be performed, they can be grouped together in braces (`{}`). -###Super States +### Super States Notice the duplication of the `Reset` transition. In all three states the `Reset` event does the same thing. It transitions to the `Locked` state and it invokes the `lock` and `alarmOff` actions. This duplication can be eliminated by using a _Super State_ as follows: Initial: Locked diff --git a/src/smc/Utilities.java b/src/smc/Utilities.java index 30617fa..ae640b8 100644 --- a/src/smc/Utilities.java +++ b/src/smc/Utilities.java @@ -14,6 +14,21 @@ public static String commaList(List names) { return commaList; } + public static String iotaList(String typeName, List names) { + String iotaList = ""; + boolean first = true; + for (String name : names) { + iotaList += "\t" + name + (first ? " " + typeName + " = iota" : "") + "\n"; + first = false; + } + return iotaList; + } + + public static String capitalize(String s) { + if (s.length() < 2) return s.toUpperCase(); + return s.substring(0, 1).toUpperCase() + s.substring(1); + } + public static List addPrefix(String prefix, List list) { List result = new ArrayList<>(); for (String element : list) diff --git a/src/smc/generators/GoCodeGenerator.java b/src/smc/generators/GoCodeGenerator.java new file mode 100644 index 0000000..c804aca --- /dev/null +++ b/src/smc/generators/GoCodeGenerator.java @@ -0,0 +1,33 @@ +package smc.generators; + +import smc.OptimizedStateMachine; +import smc.generators.nestedSwitchCaseGenerator.NSCNodeVisitor; +import smc.implementers.GoNestedSwitchCaseImplementer; + +import java.io.IOException; +import java.nio.file.Files; +import java.util.Map; + +public class GoCodeGenerator extends CodeGenerator { + private GoNestedSwitchCaseImplementer implementer; + + public GoCodeGenerator(OptimizedStateMachine optimizedStateMachine, + String outputDirectory, + Map flags) { + super(optimizedStateMachine, outputDirectory, flags); + implementer = new GoNestedSwitchCaseImplementer(flags); + } + + protected NSCNodeVisitor getImplementer() { + return implementer; + } + + public void writeFiles() throws IOException { + String outputFileName = camelToSnake(optimizedStateMachine.header.fsm + ".go"); + Files.write(getOutputPath(outputFileName), implementer.getOutput().getBytes()); + } + + private static String camelToSnake(String s) { + return s.replaceAll("([a-z])([A-Z]+)", "$1_$2").toLowerCase(); + } +} diff --git a/src/smc/generators/nestedSwitchCaseGenerator/NSCGenerator.java b/src/smc/generators/nestedSwitchCaseGenerator/NSCGenerator.java index 3439365..a8b476f 100644 --- a/src/smc/generators/nestedSwitchCaseGenerator/NSCGenerator.java +++ b/src/smc/generators/nestedSwitchCaseGenerator/NSCGenerator.java @@ -31,6 +31,7 @@ private NSCNode.FSMClassNode makeFsmNode(OptimizedStateMachine sm) { fsm.stateProperty = statePropertyNode; fsm.handleEvent = handleEventNode; fsm.actions = sm.actions; + fsm.states = sm.states; return fsm; } diff --git a/src/smc/generators/nestedSwitchCaseGenerator/NSCNode.java b/src/smc/generators/nestedSwitchCaseGenerator/NSCNode.java index 4fbcf58..71ee76e 100644 --- a/src/smc/generators/nestedSwitchCaseGenerator/NSCNode.java +++ b/src/smc/generators/nestedSwitchCaseGenerator/NSCNode.java @@ -118,6 +118,7 @@ public class FSMClassNode implements NSCNode { public String className; public String actionsName; public List actions; + public List states; public void accept(NSCNodeVisitor visitor) { visitor.visit(this); diff --git a/src/smc/implementers/GoNestedSwitchCaseImplementer.java b/src/smc/implementers/GoNestedSwitchCaseImplementer.java new file mode 100644 index 0000000..a4f9625 --- /dev/null +++ b/src/smc/implementers/GoNestedSwitchCaseImplementer.java @@ -0,0 +1,146 @@ +package smc.implementers; + +import smc.Utilities; +import smc.generators.nestedSwitchCaseGenerator.NSCNodeVisitor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static smc.generators.nestedSwitchCaseGenerator.NSCNode.*; + +public class GoNestedSwitchCaseImplementer implements NSCNodeVisitor { + private String fsmName; + private String actionsName; + private String output = ""; + private List actions = new ArrayList<>(); + private List errors = new ArrayList<>(); + private List states = new ArrayList<>(); + private Map flags; + + public GoNestedSwitchCaseImplementer(Map flags) { + this.flags = flags; + } + + public void visit(SwitchCaseNode switchCaseNode) { + output += String.format("\tswitch %s {\n", switchCaseNode.variableName); + switchCaseNode.generateCases(this); + output += "}\n"; + } + + public void visit(CaseNode caseNode) { + output += String.format("\tcase %s%s:\n", caseNode.switchName.toLowerCase(), caseNode.caseName); + caseNode.caseActionNode.accept(this); + output += "\n\n"; + } + + public void visit(FunctionCallNode functionCallNode) { + output += String.format("%s(", functionCallNode.functionName); + if (functionCallNode.argument != null) { + functionCallNode.argument.accept(this); + } + output += ")\n"; + } + + public void visit(EnumNode enumNode) { + output += String.format( + "const (\n%s)\n\n", + Utilities.iotaList( + enumNode.name.toLowerCase() + "T", + Utilities.addPrefix(enumNode.name.toLowerCase(), enumNode.enumerators))); + } + + public void visit(StatePropertyNode statePropertyNode) { + output += "state"+statePropertyNode.initialState; + } + + public void visit(EventDelegatorsNode eventDelegatorsNode) { + for (String event : eventDelegatorsNode.events) { + output += String.format("func (f *%s) %s() { f.processEvent(event%s, \"%s\") }\n", fsmName, event, event, event); + } + } + + public void visit(FSMClassNode fsmClassNode) { + if (fsmClassNode.actionsName == null) { + errors.add(Error.NO_ACTIONS); + return; + } + + fsmName = fsmClassNode.className; + actionsName = fsmClassNode.actionsName; + actions = fsmClassNode.actions; + states = fsmClassNode.states; + + output += String.format( + "// Package %s is an auto-generated Finite State Machine.\n" + + "// DO NOT EDIT.\n" + + "package %s\n\n" + + "// %s is the Finite State Machine.\n" + + "type %s struct {\n" + + "\tactions %s\n" + + "\tstate stateT\n" + + "}\n\n" + + "// New returns a new %s.\n" + + "func New(actions %s) *%s {\n" + + "\t return &%s{actions: actions, state: ", + fsmName.toLowerCase(), fsmName.toLowerCase(), fsmName, fsmName, + actionsName, fsmName, actionsName, fsmName, fsmName); + fsmClassNode.stateProperty.accept(this); + output += "}\n}\n\n"; + + fsmClassNode.delegators.accept(this); + + output += "type stateT int\n"; + fsmClassNode.stateEnum.accept(this); + output += "type eventT int\n"; + fsmClassNode.eventEnum.accept(this); + fsmClassNode.handleEvent.accept(this); + } + + public void visit(HandleEventNode handleEventNode) { + output += String.format( + "func (f *%s) processEvent(event eventT, eventName string) {\n" + + "\tstate := f.state\n" + + "\tsetState := func(s stateT) { f.state = s; state = s }\n", + fsmName); + + for (String action : actions) { + output += String.format( + "\t%s := func() { f.actions.%s() }\n", + action, Utilities.capitalize(action)); + } + output += "\n"; + + for (String state : states) { + output += String.format( + "\tconst State%s = state%s\n", + state, Utilities.capitalize(state)); + } + output += "\n"; + + handleEventNode.switchCase.accept(this); + output += "}\n\n"; + } + + public void visit(EnumeratorNode enumeratorNode) { + output += enumeratorNode.enumeration + enumeratorNode.enumerator; + } + + public void visit(DefaultCaseNode defaultCaseNode) { + output += String.format( + "" + + "\tdefault:\n" + + "\t\tf.actions.UnexpectedTransition(\"%s\", eventName);\n\n", + defaultCaseNode.state); + } + + public String getOutput() { + return output; + } + + public List getErrors() { + return errors; + } + + public enum Error {NO_ACTIONS} +} diff --git a/test_cases/go_turnstile/Makefile b/test_cases/go_turnstile/Makefile new file mode 100644 index 0000000..2a14a87 --- /dev/null +++ b/test_cases/go_turnstile/Makefile @@ -0,0 +1,12 @@ +all : clean two_coin_turnstile.go turnstile_test.go + go vet . + go test . + +clean : + rm -f two_coin_turnstile.go + +two_coin_turnstile.go : two_coin_turnstile.sm + java -jar ../../build/jar/smc.jar -l Go $? + go fmt $@ + + diff --git a/test_cases/go_turnstile/turnstile_actions.go b/test_cases/go_turnstile/turnstile_actions.go new file mode 100644 index 0000000..11c2bea --- /dev/null +++ b/test_cases/go_turnstile/turnstile_actions.go @@ -0,0 +1,11 @@ +package twocointurnstile + +// TurnstileActions represents the possible actions that can be performed. +type TurnstileActions interface { + Lock() + Unlock() + AlarmOn() + AlarmOff() + Thankyou() + UnexpectedTransition(state, event string) +} diff --git a/test_cases/go_turnstile/turnstile_test.go b/test_cases/go_turnstile/turnstile_test.go new file mode 100644 index 0000000..3ecf1f4 --- /dev/null +++ b/test_cases/go_turnstile/turnstile_test.go @@ -0,0 +1,102 @@ +package twocointurnstile + +import ( + "fmt" + "testing" +) + +var _ TurnstileActions = &MyTwoCoinTurnstile{} + +// MyTwoCoinTurnstile implements the TurnstileActions interface. +type MyTwoCoinTurnstile struct { + output string + t *testing.T +} + +func (m *MyTwoCoinTurnstile) Lock() { + m.output += "L" +} + +func (m *MyTwoCoinTurnstile) Unlock() { + m.output += "U" +} + +func (m *MyTwoCoinTurnstile) Thankyou() { + m.output += "T" +} + +func (m *MyTwoCoinTurnstile) AlarmOn() { + m.output += "A" +} + +func (m *MyTwoCoinTurnstile) AlarmOff() { + m.output += "O" +} + +func (m *MyTwoCoinTurnstile) UnexpectedTransition(state, event string) { + e := fmt.Sprintf("X(%v,%v)", state, event) + m.output += e +} + +func (m *MyTwoCoinTurnstile) check(function, want string) { + m.t.Helper() + + if m.output != want { + m.t.Errorf("%s failed: got %v, want %v", function, m.output, want) + } +} + +func setup(t *testing.T) (*TwoCoinTurnstile, *MyTwoCoinTurnstile) { + m := &MyTwoCoinTurnstile{t: t} + fsm := New(m) + return fsm, m +} + +func TestNormalBehavior(t *testing.T) { + fsm, m := setup(t) + fsm.Coin() + fsm.Coin() + fsm.Pass() + m.check("testNormalBehavior", "UL") +} + +func TestAlarm(t *testing.T) { + fsm, m := setup(t) + fsm.Pass() + m.check("testAlarm", "A") +} + +func TestThankyou(t *testing.T) { + fsm, m := setup(t) + fsm.Coin() + fsm.Coin() + fsm.Coin() + m.check("testThankyou", "UT") +} + +func TestNormalManyThanksAndAlarm(t *testing.T) { + fsm, m := setup(t) + fsm.Coin() + fsm.Coin() + fsm.Pass() + fsm.Coin() + fsm.Coin() + fsm.Coin() + fsm.Pass() + fsm.Pass() + m.check("testNormalManyThanksAndAlarm", "ULUTLA") +} + +func TestUndefined(t *testing.T) { + fsm, m := setup(t) + fsm.Pass() + fsm.Pass() + m.check("testUndefined", "AX(Alarming,Pass)") +} + +func TestReset(t *testing.T) { + fsm, m := setup(t) + fsm.Pass() + fsm.Reset() + m.check("testReset", "AOL") +} diff --git a/test_cases/go_turnstile/two_coin_turnstile.sm b/test_cases/go_turnstile/two_coin_turnstile.sm new file mode 100644 index 0000000..7131b23 --- /dev/null +++ b/test_cases/go_turnstile/two_coin_turnstile.sm @@ -0,0 +1,25 @@ +Actions: TurnstileActions +FSM: TwoCoinTurnstile +Initial: Locked +{ + (Base) Reset Locked lock + + Locked : Base { + Pass Alarming - + Coin FirstCoin - + } + + Alarming : Base alarmOff { + - - - + } + + FirstCoin : Base { + Pass Alarming - + Coin Unlocked unlock + } + + Unlocked : Base { + Pass Locked lock + Coin - thankyou + } +} \ No newline at end of file