Skip to content

Commit

Permalink
Claro Now Supports "Atom" Types!
Browse files Browse the repository at this point in the history
Atoms are an interesting idea I noticed in Portcullis by Jake Wood from the CoRecursive Slack group (although this is an idea that's implemented in various more mainstream languages such as Elixir). The concept is to create a type that has only a SINGLE value, itself. For example, if the idea of "the absence of data" were expressed as an atom, then it'd be cleaner than the current separation between the type `NothingType` and the value `nothing`.

E.g.
```
atom Nothing

newtype Tree<T> : struct {
  val: T,
  left: oneof<Tree<T>, Nothing>,   # Using `Nothing` as a type.
  right: oneof<Tree<T>, Nothing>
}

var myTree =
  Tree({
    val = 10,
    left =
      Tree({
        val = 20,
        left = Nothing,            # Using `Nothing` as a value.
        right = Nothing
      }),
    right = Nothing
  });
```

The interesting thing about these atoms is that they're "self-describing" in some sense. You could imagine that they represent some more meaningful value, but considering they're the only one of their type in the universe, it's somewhat pointless to *actually* carry that data around, after all, that data really is just some *interpretation* of the presence of such a type.

This is interesting in its own right beyond just the trivial example of making a better representation for NothingType/nothing, as this can also be the foundation of something like an "enum" that you would find in Java/C/etc.

E.g.
```
atom OutOfMemory
atom TimeLimitExceeded
atom PermissionDenied
atom UnknownInternalError

newtype MyErrorsEnum : oneof<
  OutOfMemory,
  TimeLimitExceeded,
  PermissionDenied,
  UnknownInternalError
>
```

Sidenote: As atoms are going to be particularly useful things to match on as oneof-variants. This CL includes some work at ensuring there's some initial level of support for atoms within match-case patterns. As a result I ended up finding some minor MatchStmt bugs and applying some fixes within this CL.
  • Loading branch information
JasonSteving99 committed Jun 21, 2023
1 parent 3aa0184 commit 86f0447
Show file tree
Hide file tree
Showing 17 changed files with 429 additions and 122 deletions.
1 change: 1 addition & 0 deletions src/java/com/claro/ClaroLexer.flex
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ WhiteSpace = [ \t\f]
"provider" { return symbol(Calc.PROVIDER_FUNCTION_TYPE, 0, 8, "provider"); }
"lambda" { return symbol(Calc.LAMBDA, 0, 6, "lambda"); }
"alias" { return symbol(Calc.ALIAS, 0, 5, "alias"); }
"atom" { return symbol(Calc.ATOM, 0, 4, "atom"); }
"newtype" { return symbol(Calc.NEWTYPE, 0, 7, "newtype"); }
"unwrap" { return symbol(Calc.UNWRAP, 0, 6, "unwrap"); }
"initializers" { return symbol(Calc.INITIALIZERS, 0, 12, "initializers"); }
Expand Down
10 changes: 9 additions & 1 deletion src/java/com/claro/ClaroParser.cup
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,7 @@ terminal LexedValue<String> WHILE, FOR, REPEAT, BREAK, CONTINUE, WHERE, MATCH,
terminal LexedValue<String> NOTHING_TYPE, INT_TYPE, FLOAT_TYPE, BOOLEAN_TYPE, STRING_TYPE, TUPLE_TYPE, STRUCT_TYPE,
ONEOF, FUNCTION_TYPE, CONSUMER_FUNCTION_TYPE, PROVIDER_FUNCTION_TYPE, LAMBDA;
terminal LexedValue<String> ALIAS;
terminal LexedValue<String> NEWTYPE, UNWRAP, INITIALIZERS, UNWRAPPERS;
terminal LexedValue<String> ATOM, NEWTYPE, UNWRAP, INITIALIZERS, UNWRAPPERS;
terminal LexedValue<String> RETURN, QUESTION_MARK_ASSIGNMENT;
terminal LexedValue<String> MUT;
terminal LexedValue<String> DOT;
Expand Down Expand Up @@ -376,6 +376,7 @@ nonterminal ComprehensionExpr list_comprehension_expr;
nonterminal ComprehensionExpr set_comprehension_expr;
nonterminal ComprehensionExpr map_comprehension_expr;
nonterminal AliasStmt alias_stmt;
nonterminal AtomDefinitionStmt atom_def_stmt;
nonterminal NewTypeDefStmt newtype_def_stmt;
nonterminal ImmutableList.Builder<Stmt> initializers_or_unwrappers_proc_defs_list;
nonterminal InitializersBlockStmt initializers_block_stmt;
Expand Down Expand Up @@ -609,6 +610,8 @@ stmt ::=
{: RESULT = r; :}
| alias_stmt:a
{: RESULT = a; :}
| atom_def_stmt:a
{: RESULT = a; :}
| newtype_def_stmt:n
{: RESULT = n; :}
| using_block_stmt:u
Expand Down Expand Up @@ -704,6 +707,11 @@ alias_stmt ::=
:}
;

atom_def_stmt ::=
ATOM identifier:name
{: RESULT = new AtomDefinitionStmt(name); :}
;

newtype_def_stmt ::=
NEWTYPE IDENTIFIER:type_name COLON builtin_type:base_type
{:
Expand Down
1 change: 1 addition & 0 deletions src/java/com/claro/claro_build_rules.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def claro_binary(name, srcs, java_name):
"//src/java/com/claro/stdlib",
"//src/java/com/claro/intermediate_representation/types/impls:claro_type_implementation",
"//src/java/com/claro/intermediate_representation/types/impls/builtins_impls",
"//src/java/com/claro/intermediate_representation/types/impls/builtins_impls/atoms",
"//src/java/com/claro/intermediate_representation/types/impls/builtins_impls/collections:collections_impls",
"//src/java/com/claro/intermediate_representation/types/impls/builtins_impls/futures:ClaroFuture",
"//src/java/com/claro/intermediate_representation/types/impls/builtins_impls/http:http_response",
Expand Down
11 changes: 11 additions & 0 deletions src/java/com/claro/claro_programs/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,17 @@ claro_library(
src = "Conditions.claro",
)

claro_binary(
name = "atoms",
srcs = [":atoms_lib"],
java_name = "atoms",
)

claro_library(
name = "atoms_lib",
src = "atoms.claro",
)

claro_binary(
name = "collection_comprehensions",
srcs = [":collection_comprehensions_lib"],
Expand Down
111 changes: 111 additions & 0 deletions src/java/com/claro/claro_programs/atoms.claro
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
########################################################################################################################
# The interesting thing about these atoms is that they're "self-describing" in some sense. You could imagine that they
# represent some more meaningful value, but considering they're the only one of their type in the universe, it's
# somewhat pointless to *actually* carry that data around, after all, that data really is just some *interpretation* of
# the presence of such a type.
#
# This is interesting in its own right beyond just the trivial example of making a better representation for
# NothingType/nothing, as this can also be the foundation of something like an "enum" that you would find in Java/C/etc.
########################################################################################################################

atom Nothing

newtype Tree<T> : struct {
val: T,
left: oneof<Tree<T>, Nothing>, # Using `Nothing` as a type.
right: oneof<Tree<T>, Nothing>
}

var myTree =
Tree({
val = 10,
left =
Tree({
val = 20,
left = Nothing, # Using `Nothing` as a value.
right = Nothing
}),
right = Nothing
});

print(myTree);


var n1 = Nothing;
var n2 = Nothing;

print(n1 == n2); # true

########################################################################################################################
# USING ATOMS TO REPRESENT "ENUMS".
#
# Here I'd like to demonstrate that the notion of an "Enum" in the Java/C/etc sense, is not something that needs any
# particular special language level support. The combination of atoms with oneof, makes for an enum. I see this as a
# nice validation of the utility of Claro's simple type system as it allows these *ideas* to be expressed natively
# as-needed, w/o special casing by the type system itself.
########################################################################################################################

atom OutOfMemory
atom TimeLimitExceeded
atom PermissionDenied
atom UnknownInternalError
newtype Success<T> : T

newtype FallibleOpResult<T> : oneof<
Success<T>,
Error<OutOfMemory>,
Error<TimeLimitExceeded>,
Error<PermissionDenied>,
Error<UnknownInternalError>
>

########################################################################################################################
# DEMONSTRATE USING ATOMS FOR EXPRESSIVE ERROR-HANDLING
########################################################################################################################

function doFallibleThing<T>(arg: T) -> FallibleOpResult<T> {
# To simulate something that might fail, let's just ask the user interactively what should happen.
# TODO(steving) GET THIS NESTED MATCH WORKING!
# match(input("Want an error? (Y/n)")) {
# case "Y" ->
if (input("Want an error? (Y/n)") == "Y") {
print("Ok what error in particular?\n\t1 ------- {OutOfMemory}\n\t2 ------- {TimeLimitExceeded}\n\t3 ------- {PermissionDenied}\n\tOther --- {UnknownInternalError}");
# TODO(steving) RETURNING DIRECTLY FROM CASE ACTION SHOULD WORK!
# match(input("Pick one of the above options: ")) {
# case "1" -> return FallibleOpResult(OutOfMemory);
# case "2" -> return FallibleOpResult(TimeLimitExceeded);
# case "3" -> return FallibleOpResult(PermissionDenied);
# case _ -> return FallibleOpResult(UnknownInternalError);
# }
var res: FallibleOpResult<T>;
match(input("Pick one of the above options: ")) {
case "1" ->
# TODO(steving) NEED TO FIGURE OUT HOW TO MAKE THIS TYPE INFERENCE WORK CORRECTLY.
# res = FallibleOpResult(Error(OutOfMemory));
var err = Error(OutOfMemory);
res = FallibleOpResult(err);
case "2" ->
var err = Error(TimeLimitExceeded);
res = FallibleOpResult(err);
case "3" ->
var err = Error(PermissionDenied);
res = FallibleOpResult(err);
case _ ->
var err = Error(UnknownInternalError);
res = FallibleOpResult(err);
}
return res;
}
return FallibleOpResult(Success(arg));
# case _ -> return FallibleOpResult(Success(arg));
# }
}

var fallibleOpRes = unwrap(doFallibleThing("Dummy-Task"));
match (fallibleOpRes) {
case _:Error<OutOfMemory> -> print("Ran out of memory while processing task!");
case _:Error<TimeLimitExceeded> -> print("Ran out of time while processing task!");
case _:Error<PermissionDenied> -> print("You do not have permission to perform this task!");
case _:Error<UnknownInternalError> -> print("Failed to complete this task for some unknown reason!");
case S:Success<string> -> print("Successful operation: {unwrap(S)}");
}
22 changes: 14 additions & 8 deletions src/java/com/claro/claro_programs/http_requests.claro
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ while (true) {
var parsedMove = parseMove(userInput);

match (parsedMove) {
case string ->
case _:string ->
var response = handleMove(buggyBuggiesClient, gameId, playerSecret, parsedMove);
if (response instanceof [string]) {
bestMoves = response;
}
case RepeatMove ->
case _:RepeatMove ->
var response: oneof<[string], Error<string>>;
repeat(unwrap(parsedMove).dist - 1) {
response = handleMove(buggyBuggiesClient, gameId, playerSecret, unwrap(parsedMove).dir);
Expand All @@ -99,11 +99,14 @@ while (true) {
if (response instanceof [string]) {
bestMoves = response;
}
case Error<string> ->
case _:Error<string> ->
print(parsedMove);
case Error<EndGame> ->
case _:Error<EndGame> ->
print("Good game!");
break;
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
break;
}
}

userInput = input("Which direction do you want to move? (N/E/S/W): ");
Expand All @@ -119,7 +122,7 @@ if ((bestMoves instanceof [string]) and (input("Want Claro to continue playing a
sleep(50);
var response <-| move(buggyBuggiesClient, gameId, playerSecret, bestMoves[i++]);
match (response) {
case string ->
case _:string ->
if (unwrap(cast(ParsedJson<MoveResponse>, fromJson(response))).result instanceof Error<string>) {
if (retries >= 3) {
print("Retried too many times with no success! Ending game.");
Expand Down Expand Up @@ -147,9 +150,12 @@ if ((bestMoves instanceof [string]) and (input("Want Claro to continue playing a
print("");
}
}
case Error<string> ->
case _:Error<string> ->
print("Automatic moves led to a failure!\n{response}\n\nEnding game.");
break;
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
break;
}
}
}
# For the last move, execute the move and update the best moves accordingly
Expand Down
39 changes: 30 additions & 9 deletions src/java/com/claro/claro_programs/http_server.claro
Original file line number Diff line number Diff line change
Expand Up @@ -357,28 +357,49 @@ graph function startNewGameHandler(handle: string) -> future<string> {
}

function getBestMovesHandler(buggyResponse: oneof<string, Error<string>>) -> string {
match (buggyResponse) {
case string ->
# TODO(steving) TESTING!!! SUPPORT NESTED MATCHES!
# match (buggyResponse) {
# case _:string ->
if (buggyResponse instanceof string) {
var moveResponse = unwrap(cast(ParsedJson<MoveResponse>, fromJson(buggyResponse))).result;
match (moveResponse) {
case MoveResponse ->
case _:MoveResponse ->
var parsedWorld = parseWorldMap(moveResponse.result.world);
var computedBestPath = dijkstra(parsedWorld, Position({x = moveResponse.result.you.x, y = moveResponse.result.you.y}));
var computedMoves = ["\"{dir}\"" | dir in movesFromBestPath(computedBestPath)];
return "{computedMoves}";
case Error<string> ->
return "FAILED TO PARSE CURRENT STATE OF THE WORLD:\n{moveResponse}";
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
return "{computedMoves}";
}
case _:Error<string> ->
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
return "FAILED TO PARSE CURRENT STATE OF THE WORLD:\n{moveResponse}";
}
}
case Error<string> ->
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
return "SHOULDN'T BE NECESSARY!";
} else {
# case _:Error<string> ->
return "FAILED TO PARSE CURRENT STATE OF THE WORLD:\n{buggyResponse}";
}
}

function handleBuggyResponseAsHtmlStrParts(buggyResponse: oneof<string, Error<string>>) -> [string] {
match (buggyResponse) {
case string -> return [buggyResponse];
case Error<string> -> return ["GOT THE FOLLOWING ERROR:\n", unwrap(buggyResponse)];
case _:string ->
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
return [buggyResponse];
}
case _:Error<string> ->
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
if (true) {
return ["GOT THE FOLLOWING ERROR:\n", unwrap(buggyResponse)];
}
}
# TODO(steving) TESTING!!! NEED TO FIGURE OUT HOW TO SUPPORT BREAK/RETURN W/IN CASE BODIES!
return ["SHOULDN'T BE NECESSARY!"];
}

function reduce<T, R>(l: [T], accumulated: R, accumulatorFn: function<|R, T| -> R>) -> R {
Expand Down
10 changes: 9 additions & 1 deletion src/java/com/claro/intermediate_representation/ProgramNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,9 @@ private void performTypeDiscoveryPhase(StmtListNode stmtListNode, ScopedHeap sco
StmtListNode currStmtListNode = stmtListNode;
while (currStmtListNode != null) {
Stmt currStmt = (Stmt) currStmtListNode.getChildren().get(0);
if (currStmt instanceof UserDefinedTypeDefinitionStmt) {
if (currStmt instanceof AtomDefinitionStmt) {
((AtomDefinitionStmt) currStmt).registerType(scopedHeap);
} else if (currStmt instanceof UserDefinedTypeDefinitionStmt) {
((UserDefinedTypeDefinitionStmt) currStmt).registerTypeProvider(scopedHeap);
} else if (currStmt instanceof HttpServiceDefStmt) {
((HttpServiceDefStmt) currStmt).registerTypeProvider(scopedHeap);
Expand Down Expand Up @@ -495,6 +497,7 @@ private StringBuilder genJavaSource(Node.GeneratedJavaSource stmtListJavaSource)
"import com.claro.intermediate_representation.types.impls.ClaroTypeImplementation;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.*;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.collections.*;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.atoms.$ClaroAtom;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.futures.ClaroFuture;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.http.$ClaroHttpResponse;\n" +
"import com.claro.intermediate_representation.types.impls.builtins_impls.procedures.ClaroConsumerFunction;\n" +
Expand Down Expand Up @@ -539,7 +542,11 @@ private StringBuilder genJavaSource(Node.GeneratedJavaSource stmtListJavaSource)
"%s\n\n" +
" public static void main(String[] args) {\n" +
" try {\n" +
" // Setup the atom cache so that all atoms are singleton.\n" +
" %s\n\n" +
"/**BEGIN USER CODE**/\n" +
"%s\n\n" +
"/**END USER CODE**/\n" +
" } finally {\n" +
" // Because Claro has native support for Graph Functions which execute concurrently/asynchronously,\n" +
" // we also need to make sure to shutdown the executor service at the end of the run to clean up.\n" +
Expand All @@ -555,6 +562,7 @@ private StringBuilder genJavaSource(Node.GeneratedJavaSource stmtListJavaSource)
this.generatedClassName,
stmtListJavaSource.optionalStaticPreambleStmts().orElse(new StringBuilder()),
stmtListJavaSource.optionalStaticDefinitions().orElse(new StringBuilder()),
AtomDefinitionStmt.codegenAtomCacheInit(),
stmtListJavaSource.javaSourceBody()
)
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,13 +260,19 @@ public static Type validateArgExprsAndExtractConcreteGenericTypeParams(
}

return Types.StructType.forFieldTypes(actualStructType.getFieldNames(), validatedFieldTypesBuilder.build(), actualStructType.isMutable());
case USER_DEFINED_TYPE:
// UserDefinedTypes also have a name to be compared.
if (!((Types.UserDefinedType) functionExpectedArgType).getTypeName()
.equals(((Types.UserDefinedType) actualArgExprType).getTypeName())) {
throw DEFAULT_TYPE_MISMATCH_EXCEPTION;
}
// But then can go ahead and fallthrough to the generic handling of parameterized types.
case FUTURE: // TODO(steving) Actually, all types should be able to be validated in this way... THIS is how I had originally set out to implement Types
case LIST: // as nested structures that self-describe. If they all did this, there could be a single case instead of a switch.
case MAP:
case SET:
case TUPLE:
case HTTP_SERVER:
case USER_DEFINED_TYPE:
ImmutableList<Type> expectedParameterizedArgTypes =
functionExpectedArgType.parameterizedTypeArgs().values().asList();
ImmutableList<Type> actualParameterizedArgTypes = actualArgExprType.parameterizedTypeArgs().values().asList();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,14 @@ public StringBuilder generateJavaSourceBodyOutput(ScopedHeap scopedHeap) {
return new StringBuilder(
this.alternateCodegenString.orElse(
() -> {
if (InternalStaticStateUtil.ComprehensionExpr_nestedComprehensionIdentifierReferences.contains(this.identifier)) {
if (scopedHeap.getValidatedIdentifierType(this.identifier).baseType().equals(BaseType.ATOM)
&& scopedHeap.getIdentifierData(this.identifier).isTypeDefinition) {
// Here it turns out that we actually need to codegen a lookup into the ATOM CACHE.
return String.format(
"$ClaroAtom.forCacheIndex(%s)",
InternalStaticStateUtil.AtomDefinition_CACHE_INDEX_BY_ATOM_NAME.build().get(this.identifier)
);
} else if (InternalStaticStateUtil.ComprehensionExpr_nestedComprehensionIdentifierReferences.contains(this.identifier)) {
// Nested comprehension Exprs depend on a synthetic class wrapping the nested identifier refs to
// workaround Java's restriction that all lambda captures must be effectively final.
return "$nestedComprehensionState." + this.identifier;
Expand Down
Loading

0 comments on commit 86f0447

Please sign in to comment.