Skip to content

Commit

Permalink
Adds implementations of if_* special forms (#957)
Browse files Browse the repository at this point in the history
  • Loading branch information
popematt authored Oct 5, 2024
1 parent f9f72f0 commit ebb3b65
Show file tree
Hide file tree
Showing 3 changed files with 130 additions and 6 deletions.
6 changes: 6 additions & 0 deletions src/main/java/com/amazon/ion/impl/macro/Macro.kt
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,12 @@ enum class SystemMacro(val macroName: String, override val signature: List<Macro
),

// TODO: Other system macros

// Technically not system macros, but special forms. However, it's easier to model them as if they are macros in TDL.
IfNone("IfNone", listOf(zeroToManyTagged("stream"), zeroToManyTagged("true_branch"), zeroToManyTagged("false_branch"))),
IfSome("IfSome", listOf(zeroToManyTagged("stream"), zeroToManyTagged("true_branch"), zeroToManyTagged("false_branch"))),
IfSingle("IfSingle", listOf(zeroToManyTagged("stream"), zeroToManyTagged("true_branch"), zeroToManyTagged("false_branch"))),
IfMulti("IfMulti", listOf(zeroToManyTagged("stream"), zeroToManyTagged("true_branch"), zeroToManyTagged("false_branch"))),
;

override val dependencies: List<Macro>
Expand Down
53 changes: 51 additions & 2 deletions src/main/java/com/amazon/ion/impl/macro/MacroEvaluator.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,17 @@ class MacroEvaluator {
} else {
throw IonException("Too many values for argument $argName")
}
true // Continue expansion
}
return value
}

/**
* Reads the expanded values from one argument.
*
* The callback should return true to continue the expansion or false to abandon the expansion early.
*/
fun readExpandedArgumentValues(expansionInfo: ExpansionInfo, macroEvaluator: MacroEvaluator, callback: (DataModelExpression) -> Unit) {
fun readExpandedArgumentValues(expansionInfo: ExpansionInfo, macroEvaluator: MacroEvaluator, callback: (DataModelExpression) -> Boolean) {
val i = expansionInfo.i
expansionInfo.nextSourceExpression()

Expand All @@ -74,10 +77,16 @@ class MacroEvaluator {

val depth = macroEvaluator.expansionStack.size()
var expr = macroEvaluator.expandNext(depth)
var continueExpansion: Boolean
while (expr != null) {
callback(expr)
continueExpansion = callback(expr)
if (!continueExpansion) break
expr = macroEvaluator.expandNext(depth)
}
// Step back out to the original depth (in case we exited the expansion early)
while (macroEvaluator.expansionStack.size() > depth) {
macroEvaluator.expansionStack.pop()
}
}
}

Expand Down Expand Up @@ -122,6 +131,7 @@ class MacroEvaluator {
is DataModelValue -> throw IonException("Invalid argument type for 'make_string': ${it.type}")
is FieldName -> TODO("Unreachable. We shouldn't be able to get here without first encountering a StructValue.")
}
true // continue expansion
}
return StringValue(value = sb.toString())
}
Expand All @@ -138,6 +148,7 @@ class MacroEvaluator {
is DataModelValue -> throw IonException("Invalid argument type for 'make_symbol': ${it.type}")
is FieldName -> TODO("Unreachable. We shouldn't be able to get here without first encountering a StructValue.")
}
true // continue expansion
}
return SymbolValue(value = newSymbolToken(sb.toString()))
}
Expand All @@ -158,6 +169,36 @@ class MacroEvaluator {
}
}

private enum class IfExpander(private val minInclusive: Int, private val maxExclusive: Int) : Expander {
IF_NONE(0, 1),
IF_SOME(1, -1),
IF_SINGLE(1, 2),
IF_MULTI(2, -1),
;

override fun nextExpression(expansionInfo: ExpansionInfo, macroEvaluator: MacroEvaluator): Expression {
var n = 0
readExpandedArgumentValues(expansionInfo, macroEvaluator) {
n++
// If there's no max, then we'll only continue the expansion if we haven't yet reached the min
// If there is a max, then we'll continue the expansion until we reach the max
if (maxExclusive < 0) n < minInclusive else n < maxExclusive
}
val isConditionTrue = n >= minInclusive && (maxExclusive < 0 || n < maxExclusive)
// Save the current expansion index. This is the index of the "true" expression
val trueExpressionPosition = expansionInfo.i
// Now we are positioned on the "false" expression
expansionInfo.nextSourceExpression()
if (isConditionTrue) {
// If the condition is true, we can set the EXCLUSIVE END of this expansion to the position of the
// "false" expression, and then we reset the current index to the position of the "true" expression.
expansionInfo.endExclusive = expansionInfo.i
expansionInfo.i = trueExpressionPosition
}
return expansionInfo.nextSourceExpression()
}
}

private enum class ExpansionKind(val expander: Expander) {
Container(SimpleExpander),
TemplateBody(SimpleExpander),
Expand All @@ -166,6 +207,10 @@ class MacroEvaluator {
MakeString(MakeStringExpander),
MakeSymbol(MakeSymbolExpander),
MakeDecimal(MakeDecimalExpander),
IfNone(IfExpander.IF_NONE),
IfSome(IfExpander.IF_SOME),
IfSingle(IfExpander.IF_SINGLE),
IfMulti(IfExpander.IF_MULTI),
;

companion object {
Expand All @@ -178,6 +223,10 @@ class MacroEvaluator {
SystemMacro.MakeString -> MakeString
SystemMacro.MakeSymbol -> MakeSymbol
SystemMacro.MakeDecimal -> MakeDecimal
SystemMacro.IfNone -> IfNone
SystemMacro.IfSome -> IfSome
SystemMacro.IfSingle -> IfSingle
SystemMacro.IfMulti -> IfMulti
}
}
}
Expand Down
77 changes: 73 additions & 4 deletions src/test/java/com/amazon/ion/impl/macro/MacroEvaluatorTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments.arguments
import org.junit.jupiter.params.provider.MethodSource

class MacroEvaluatorTest {

Expand All @@ -35,7 +38,7 @@ class MacroEvaluatorTest {
}
}

val ABCs_MACRO = template() {
val ABCs_LIST_MACRO = template {
list {
string("a")
string("b")
Expand Down Expand Up @@ -186,7 +189,7 @@ class MacroEvaluatorTest {
// [ "a", "b", "c" ]

evaluator.initExpansion {
eexp(ABCs_MACRO) {}
eexp(ABCs_LIST_MACRO) {}
}

assertIsInstance<ListValue>(evaluator.expandNext())
Expand All @@ -209,7 +212,7 @@ class MacroEvaluatorTest {
// [ "a", "b", "c" ]

evaluator.initExpansion {
eexp(ABCs_MACRO) {}
eexp(ABCs_LIST_MACRO) {}
}

assertIsInstance<ListValue>(evaluator.expandNext())
Expand Down Expand Up @@ -786,8 +789,74 @@ class MacroEvaluatorTest {
assertEquals(null, evaluator.expandNext())
}

companion object {
object IfExpanderTestParameters {
val SINGLE_VALUE = template { int(1) }
val SINGLE_VALUE_STREAM = template {
macro(Values) {
expressionGroup {
int(2)
}
}
}
val TWO_VALUE_STREAM = template {
macro(Values) {
expressionGroup {
int(3)
int(4)
}
}
}

@JvmStatic
fun parameters() = listOf(
arguments(IfNone, None, true),
arguments(IfNone, SINGLE_VALUE, false),
arguments(IfNone, SINGLE_VALUE_STREAM, false),
arguments(IfNone, TWO_VALUE_STREAM, false),

arguments(IfSome, None, false),
arguments(IfSome, SINGLE_VALUE, true),
arguments(IfSome, SINGLE_VALUE_STREAM, true),
arguments(IfSome, TWO_VALUE_STREAM, true),

arguments(IfSingle, None, false),
arguments(IfSingle, SINGLE_VALUE, true),
arguments(IfSingle, SINGLE_VALUE_STREAM, true),
arguments(IfSingle, TWO_VALUE_STREAM, false),

arguments(IfMulti, None, false),
arguments(IfMulti, SINGLE_VALUE, false),
arguments(IfMulti, SINGLE_VALUE_STREAM, false),
arguments(IfMulti, TWO_VALUE_STREAM, true),
)
}

@ParameterizedTest
@MethodSource("com.amazon.ion.impl.macro.MacroEvaluatorTest\$IfExpanderTestParameters#parameters")
fun `check 'if' expansion logic`(ifSpecialForm: SystemMacro, expressionToTest: Macro, expectMatches: Boolean) {
// Given:
// (macro test_if (x*) (<ifSpecialForm> (%x) "a" "b"))
// When:
// (:test_if <expressionToTest>)
// Then:
// "a" or "b" depending on whether we expect it to match.

val theMacro = template("x*") {
macro(ifSpecialForm) {
variable(0)
string("a")
string("b")
}
}

evaluator.initExpansion { eexp(theMacro) { eexp(expressionToTest) {} } }

val expectedString = if (expectMatches) "a" else "b"
assertEquals(StringValue(value = expectedString), evaluator.expandNext())
assertEquals(null, evaluator.expandNext())
}

companion object {
/** Helper function to create template macros */
fun template(vararg parameters: String, body: TemplateDsl.() -> Unit): Macro {
val signature = parameters.map {
Expand Down

0 comments on commit ebb3b65

Please sign in to comment.