Skip to content

Commit eb221c4

Browse files
committed
Add fold function to smooth standard library
1 parent c441398 commit eb221c4

File tree

31 files changed

+766
-46
lines changed

31 files changed

+766
-46
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Version ?????? (??????????)
66
---------------------------
77

88
* extend smooth syntax: Allow omitting parentheses in function type when it has one parameter
9+
* added standard library function `fold`
910

1011
Version 0.23.0 (2025.03.20)
1112
---------------------------
@@ -226,4 +227,3 @@ Version 0.1.0 (2013.09.23)
226227
--------------------------
227228

228229
* initial release
229-

doc/api.md

Lines changed: 32 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,35 @@
1-
2-
31
#### Smooth standard library
42

3+
| function | definition |
4+
|-----------------------------------|---------------------------------------------------------------------|
5+
| [and](api/and.md) | Returns `true` if both arguments are `true`. |
6+
| [concat](api/concat.md) | Concatenates an array of arrays. |
7+
| [elem](api/elem.md) | Returns array element. |
8+
| [error](api/error.md) | Fails build with error. |
9+
| [equal](api/equal.md) | Returns `true` when arguments are equal. |
10+
| [file](api/file.md) | Reads single file from project filesystem. |
11+
| [files](api/files.md) | Reads files from project filesystem. |
12+
| [File](api/File.md) | Creates File from path and content. |
13+
| [filter](api/filter.md) | Filters array using predicate. |
14+
| [filterFiles](api/filterFiles.md) | Filters files according to glob pattern. |
15+
| [fold](api/fold.md) | Applies a function to each element of an array with an accumulator. |
16+
| [id](api/id.md) | Identity function - it returns its only argument. |
17+
| [if](api/if.md) | Returns one of two values depending on bool condition. |
18+
| [jar](api/jar.md) | Jars (compresses) files. |
19+
| [jarFile](api/jarFile.md) | Creates jar file with (compressed) files. |
20+
| [javac](api/javac.md) | Compiles java files. |
21+
| [junit](api/junit.md) | Runs junit tests. |
22+
| [map](api/map.md) | Applies given function to each element of an array. |
23+
| [not](api/not.md) | Returns negation of its argument. |
24+
| [or](api/or.md) | Returns `true` if any argument is `true`. |
25+
| [size](api/size.md) | Returns size of an array. |
26+
| [toBlob](api/toBlob.md) | Converts String to Blob using UTF-8 encoding. |
27+
| [toString](api/toString.md) | Converts Blob to String using UTF-8 encoding. |
28+
| [unjar](api/unjar.md) | Unjars (uncompresses) files from given jar file. |
29+
| [unzip](api/unzip.md) | Unzips files from zip file. |
30+
| [zip](api/zip.md) | Zips (compresses) files. |
531

6-
| function | definition |
7-
|-----------------------------------|--------------------------------------------------------|
8-
| [and](api/and.md) | Returns `true` if both arguments are `true`. |
9-
| [concat](api/concat.md) | Concatenates an array of arrays. |
10-
| [elem](api/elem.md) | Returns array element. |
11-
| [error](api/error.md) | Fails build with error. |
12-
| [equal](api/equal.md) | Returns `true` when arguments are equal. |
13-
| [file](api/file.md) | Reads single file from project filesystem. |
14-
| [files](api/files.md) | Reads files from project filesystem. |
15-
| [File](api/File.md) | Creates File from path and content. |
16-
| [filter](api/filter.md) | Filters array using predicate. |
17-
| [filterFiles](api/filterFiles.md) | Filters files according to glob pattern. |
18-
| [id](api/id.md) | Identity function - it returns its only argument. |
19-
| [if](api/if.md) | Returns one of two values depending on bool condition. |
20-
| [jar](api/jar.md) | Jars (compresses) files. |
21-
| [jarFile](api/jarFile.md) | Creates jar file with (compressed) files. |
22-
| [javac](api/javac.md) | Compiles java files. |
23-
| [junit](api/junit.md) | Runs junit tests. |
24-
| [map](api/map.md) | Applies given function to each element of an array. |
25-
| [not](api/not.md) | Returns negation of its argument. |
26-
| [or](api/or.md) | Returns `true` if any argument is `true`. |
27-
| [size](api/size.md) | Returns size of an array. |
28-
| [toBlob](api/toBlob.md) | Converts String to Blob using UTF-8 encoding. |
29-
| [toString](api/toString.md) | Converts Blob to String using UTF-8 encoding. |
30-
| [unjar](api/unjar.md) | Unjars (uncompresses) files from given jar file. |
31-
| [unzip](api/unzip.md) | Unzips files from zip file. |
32-
| [zip](api/zip.md) | Zips (compresses) files. |
33-
34-
| value | definition |
35-
|-----------------------|----------------------------|
36-
| false | `false` value of type Bool |
37-
| true | `true` value of type Bool |
32+
| value | definition |
33+
|-------|----------------------------|
34+
| false | `false` value of type Bool |
35+
| true | `true` value of type Bool |

doc/api/fold.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
## A fold<A, E>([E] array, A initial, (A, E)->A folder)
2+
3+
Returns a value created by applying `folder` function to each element of `array`, starting with `initial` value.
4+
5+
| Name | Type | Default | Description |
6+
|----------|-----------|---------|--------------------------------------------------------------------------------------------|
7+
| array | [E] | | Array with elements to fold. |
8+
| initial | A | | Initial value for the accumulator. |
9+
| folder | (A, E)->A | | Function that takes the current accumulator value and an element, and returns a new value. |
10+
11+
Returns a value created by applying `folder` function to each element of `array`, starting with `initial` value.

doc/tutorial.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,5 +377,4 @@ Such a change is also more readable in version control.
377377

378378
### Things not yet implemented
379379

380-
- `fold` function (`B fold([A] array, (A,B)->B func, B zero)`)
381380
- modules and imports so functions/values do not pollute global namespace
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package org.smoothbuild.stdlib.core;
2+
3+
import java.math.BigInteger;
4+
import java.util.Map;
5+
import org.smoothbuild.virtualmachine.bytecode.BytecodeException;
6+
import org.smoothbuild.virtualmachine.bytecode.BytecodeFactory;
7+
import org.smoothbuild.virtualmachine.bytecode.expr.base.BValue;
8+
import org.smoothbuild.virtualmachine.bytecode.kind.base.BType;
9+
10+
/**
11+
* A fold([E] array, A initial, (A,E)->A folder)
12+
*/
13+
public class FoldFunc {
14+
public static BValue bytecode(BytecodeFactory f, Map<String, BType> varMap)
15+
throws BytecodeException {
16+
var a = varMap.get("A");
17+
var e = varMap.get("E");
18+
19+
var resultType = a;
20+
var arrayParamType = f.arrayType(e);
21+
var initialParamType = a;
22+
var folderParamType = f.lambdaType(f.tupleType(a, e), a);
23+
var parameterTypes = f.tupleType(arrayParamType, initialParamType, folderParamType);
24+
25+
var arrayParamReference = f.reference(arrayParamType, f.int_(BigInteger.ZERO));
26+
var initialParamReference = f.reference(initialParamType, f.int_(BigInteger.ONE));
27+
var folderParamReference = f.reference(folderParamType, f.int_(BigInteger.TWO));
28+
29+
var funcType = f.lambdaType(parameterTypes, resultType);
30+
var body = f.fold(arrayParamReference, initialParamReference, folderParamReference);
31+
return f.lambda(funcType, body);
32+
}
33+
}

src/standard-library/src/main/smooth/std_lib.smooth

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ A if<A>(Bool condition, A then, A else);
9393
@Bytecode("org.smoothbuild.stdlib.core.MapFunc")
9494
[R] map<S,R>([S] array, (S)->R mapper);
9595

96+
@Bytecode("org.smoothbuild.stdlib.core.FoldFunc")
97+
A fold<A,E>([E] array, A initial, (A,E)->A folder);
98+
9699
@Bytecode("org.smoothbuild.stdlib.bool.False")
97100
Bool false;
98101

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package org.smoothbuild.stdlib.common;
2+
3+
import static com.google.common.truth.Truth.assertThat;
4+
5+
import org.junit.jupiter.api.Test;
6+
import org.smoothbuild.stdlib.StandardLibraryTestContext;
7+
import org.smoothbuild.virtualmachine.testing.func.nativ.ConcatStrings;
8+
9+
public class FoldTest extends StandardLibraryTestContext {
10+
@Test
11+
void folding_strings_with_concat() throws Exception {
12+
var code = String.format(
13+
"""
14+
@Native("%s")
15+
String concatStrings(String a, String b);
16+
result = fold(["a", "b", "c"], "", concatStrings);
17+
""",
18+
ConcatStrings.class.getCanonicalName());
19+
createUserModule(code, ConcatStrings.class);
20+
evaluate("result");
21+
assertThat(artifact()).isEqualTo(bString("abc"));
22+
}
23+
24+
@Test
25+
void folding_empty_array_returns_initial_value() throws Exception {
26+
var code = String.format(
27+
"""
28+
@Native("%s")
29+
String concatStrings(String a, String b);
30+
result = fold([], "initial", concatStrings);
31+
""",
32+
ConcatStrings.class.getCanonicalName());
33+
createUserModule(code, ConcatStrings.class);
34+
evaluate("result");
35+
assertThat(artifact()).isEqualTo(bString("initial"));
36+
}
37+
38+
@Test
39+
void and_booleans() throws Exception {
40+
var code = """
41+
result = fold([true, true, false, true], true, and);
42+
""";
43+
createUserModule(code);
44+
evaluate("result");
45+
assertThat(artifact()).isEqualTo(bBool(false));
46+
}
47+
}

src/virtual-machine/src/main/java/org/smoothbuild/virtualmachine/bytecode/BytecodeFactory.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.smoothbuild.virtualmachine.bytecode.expr.base.BChoose;
2929
import org.smoothbuild.virtualmachine.bytecode.expr.base.BCombine;
3030
import org.smoothbuild.virtualmachine.bytecode.expr.base.BExpr;
31+
import org.smoothbuild.virtualmachine.bytecode.expr.base.BFold;
3132
import org.smoothbuild.virtualmachine.bytecode.expr.base.BIf;
3233
import org.smoothbuild.virtualmachine.bytecode.expr.base.BInt;
3334
import org.smoothbuild.virtualmachine.bytecode.expr.base.BInvoke;
@@ -119,6 +120,10 @@ public BTuple file(BBlob content, BString path) throws BytecodeException {
119120
return exprDb.newTuple(list(content, path));
120121
}
121122

123+
public BFold fold(BExpr array, BExpr initial, BExpr folder) throws BytecodeException {
124+
return exprDb.newFold(array, initial, folder);
125+
}
126+
122127
public BIf if_(BExpr condition, BExpr then_, BExpr else_) throws BytecodeException {
123128
return exprDb.newIf(condition, then_, else_);
124129
}

src/virtual-machine/src/main/java/org/smoothbuild/virtualmachine/bytecode/expr/BExprDb.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.smoothbuild.virtualmachine.bytecode.expr.base.BChoose;
2222
import org.smoothbuild.virtualmachine.bytecode.expr.base.BCombine;
2323
import org.smoothbuild.virtualmachine.bytecode.expr.base.BExpr;
24+
import org.smoothbuild.virtualmachine.bytecode.expr.base.BFold;
2425
import org.smoothbuild.virtualmachine.bytecode.expr.base.BIf;
2526
import org.smoothbuild.virtualmachine.bytecode.expr.base.BInt;
2627
import org.smoothbuild.virtualmachine.bytecode.expr.base.BInvoke;
@@ -161,6 +162,41 @@ public BCombine newCombine(List<? extends BExpr> items) throws BytecodeException
161162
return kind.newExpr(root, this);
162163
}
163164

165+
public BFold newFold(BExpr array, BExpr initial, BExpr folder) throws BytecodeException {
166+
var resultType = validateFoldSubExprsAndGetResultType(array, initial, folder);
167+
var kind = kindDb.fold(resultType);
168+
var data = writeChain(array.hash(), initial.hash(), folder.hash());
169+
var root = newRoot(kind, data);
170+
return kind.newExpr(root, this);
171+
}
172+
173+
private BType validateFoldSubExprsAndGetResultType(BExpr array, BExpr initial, BExpr folder)
174+
throws BKindDbException {
175+
var lambdaType = validateMemberEvaluationTypeClass("folder", folder, BLambdaType.class);
176+
var arrayType = validateMemberEvaluationTypeClass("array", array, BArrayType.class);
177+
var initialType = initial.evaluationType();
178+
var params = lambdaType.params().elements();
179+
var elementType = arrayType.element();
180+
if (!(params.size() == 2
181+
&& params.get(0).equals(initialType)
182+
&& params.get(1).equals(elementType))) {
183+
throw illegalEvaluationType(
184+
"folder.arguments",
185+
expectedFolderArgumentsType(initialType, elementType),
186+
lambdaType.params());
187+
}
188+
var resultType = lambdaType.result();
189+
if (!resultType.equals(initialType)) {
190+
throw illegalEvaluationType("folder.result", initialType, resultType);
191+
}
192+
return resultType;
193+
}
194+
195+
private BTupleType expectedFolderArgumentsType(BType accumulatorType, BType elementType)
196+
throws BKindDbException {
197+
return kindDb.tuple(accumulatorType, elementType);
198+
}
199+
164200
public BIf newIf(BExpr condition, BExpr then_, BExpr else_) throws BytecodeException {
165201
validateMemberEvaluationType("condition", condition, kindDb.bool());
166202
validateMemberEvaluationType("then", then_, else_.evaluationType());
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package org.smoothbuild.virtualmachine.bytecode.expr.base;
2+
3+
import static com.google.common.base.Preconditions.checkArgument;
4+
import static org.smoothbuild.common.collect.List.list;
5+
6+
import org.smoothbuild.common.base.ToStringBuilder;
7+
import org.smoothbuild.common.collect.List;
8+
import org.smoothbuild.virtualmachine.bytecode.BytecodeException;
9+
import org.smoothbuild.virtualmachine.bytecode.expr.BExprDb;
10+
import org.smoothbuild.virtualmachine.bytecode.expr.MerkleRoot;
11+
import org.smoothbuild.virtualmachine.bytecode.expr.exc.MemberHasWrongEvaluationTypeException;
12+
import org.smoothbuild.virtualmachine.bytecode.kind.base.BArrayType;
13+
import org.smoothbuild.virtualmachine.bytecode.kind.base.BFoldKind;
14+
import org.smoothbuild.virtualmachine.bytecode.kind.base.BType;
15+
16+
/**
17+
* 'Fold' function.
18+
* This class is thread-safe.
19+
*/
20+
public final class BFold extends BOperation {
21+
public BFold(MerkleRoot merkleRoot, BExprDb exprDb) {
22+
super(merkleRoot, exprDb);
23+
checkArgument(merkleRoot.kind() instanceof BFoldKind);
24+
}
25+
26+
@Override
27+
public BFoldKind kind() {
28+
return (BFoldKind) super.kind();
29+
}
30+
31+
@Override
32+
public BType evaluationType() {
33+
return kind().evaluationType();
34+
}
35+
36+
@Override
37+
public BSubExprs subExprs() throws BytecodeException {
38+
var hashes = readDataAsHashChain(3);
39+
var array = readMemberFromHashChain(hashes, 0);
40+
var arrayEvaluationType = array.evaluationType();
41+
if (!(arrayEvaluationType instanceof BArrayType arrayType)) {
42+
throw new MemberHasWrongEvaluationTypeException(
43+
hash(), kind(), "array", BArrayType.class.getSimpleName(), arrayEvaluationType);
44+
}
45+
var initial = readMemberFromHashChain(hashes, 1);
46+
var initialEvaluationType = initial.evaluationType();
47+
var folder = readMemberFromHashChain(hashes, 2);
48+
var folderEvaluationType = folder.evaluationType();
49+
var expectedFolderEvaluationType =
50+
kindDb().lambda(list(initialEvaluationType, arrayType.element()), initialEvaluationType);
51+
if (!(folderEvaluationType.equals(expectedFolderEvaluationType))) {
52+
throw new MemberHasWrongEvaluationTypeException(
53+
hash(), kind(), "folder", expectedFolderEvaluationType, folderEvaluationType);
54+
}
55+
return new BSubExprs(array, initial, folder);
56+
}
57+
58+
@Override
59+
public String exprToString() throws BytecodeException {
60+
var subExprs = subExprs();
61+
return new ToStringBuilder(getClass().getSimpleName())
62+
.addField("hash", hash())
63+
.addField("evaluationType", evaluationType())
64+
.addField("array", subExprs.array())
65+
.addField("initial", subExprs.initial())
66+
.addField("folder", subExprs.folder())
67+
.toString();
68+
}
69+
70+
public static record BSubExprs(BExpr array, BExpr initial, BExpr folder) implements BExprs {
71+
@Override
72+
public List<BExpr> toList() {
73+
return list(array, initial, folder);
74+
}
75+
}
76+
}

0 commit comments

Comments
 (0)