diff --git a/rune-runtime/src/main/java/com/rosetta/model/lib/reports/ReportFunction.java b/rune-runtime/src/main/java/com/rosetta/model/lib/reports/ReportFunction.java
new file mode 100644
index 000000000..afcbcc718
--- /dev/null
+++ b/rune-runtime/src/main/java/com/rosetta/model/lib/reports/ReportFunction.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2024 REGnosys
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rosetta.model.lib.reports;
+
+import com.rosetta.model.lib.functions.RosettaFunction;
+
+public interface ReportFunction extends RosettaFunction {
+ Report evaluate(Input reportableInput);
+}
diff --git a/rune-runtime/src/main/java/com/rosetta/model/lib/reports/Tabulator.java b/rune-runtime/src/main/java/com/rosetta/model/lib/reports/Tabulator.java
new file mode 100644
index 000000000..6b9a2c3a0
--- /dev/null
+++ b/rune-runtime/src/main/java/com/rosetta/model/lib/reports/Tabulator.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright 2024 REGnosys
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.rosetta.model.lib.reports;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.apache.commons.lang3.Validate;
+
+import com.rosetta.model.lib.ModelSymbolId;
+
+public interface Tabulator {
+
+ List getFields();
+ List tabulate(T report);
+
+ public interface Field {
+ String getName();
+ String getAttributeName();
+ List getChildren();
+ boolean isMulti();
+ }
+ public interface FieldValue {
+ Field getField();
+ Optional extends Object> getValue();
+
+ default boolean isPresent() {
+ return getValue().isPresent();
+ }
+
+ default void accept(FieldValueVisitor visitor, C context) {
+ visitor.visitSingle(this, context);
+ }
+ }
+ public interface NestedFieldValue extends FieldValue {
+ Optional extends List extends FieldValue>> getValue();
+
+ default void accept(FieldValueVisitor visitor, C context) {
+ visitor.visitNested(this, context);
+ }
+ }
+ public interface MultiNestedFieldValue extends FieldValue {
+ Optional extends List extends List extends FieldValue>>> getValue();
+
+ default void accept(FieldValueVisitor visitor, C context) {
+ visitor.visitMultiNested(this, context);
+ }
+ }
+ public interface FieldValueVisitor {
+ void visitSingle(FieldValue fieldValue, C context);
+ void visitNested(NestedFieldValue fieldValue, C context);
+ void visitMultiNested(MultiNestedFieldValue fieldValue, C context);
+ }
+
+ public static class FieldImpl implements Field {
+ private String attributeName;
+
+ private boolean isMulti;
+ private Optional ruleId;
+ private Optional identifier;
+ private List children;
+
+ public FieldImpl(String attributeName, boolean isMulti, Optional ruleId, Optional identifier, List children) {
+ Objects.requireNonNull(ruleId);
+ Objects.requireNonNull(attributeName);
+ Objects.requireNonNull(identifier);
+ Validate.noNullElements(children);
+ this.ruleId = ruleId;
+ this.attributeName = attributeName;
+ this.isMulti = isMulti;
+ this.identifier = identifier;
+ this.children = children;
+ }
+
+ @Override
+ public String getName() {
+ return identifier.orElse(attributeName);
+ }
+
+ @Override
+ public String getAttributeName() {
+ return attributeName;
+ }
+
+ @Override
+ public boolean isMulti() {
+ return isMulti;
+ }
+
+ public Optional getRuleId() {
+ return ruleId;
+ }
+
+ @Override
+ public List getChildren() {
+ return children;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(attributeName, children, identifier, isMulti, ruleId);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ FieldImpl other = (FieldImpl) obj;
+ return Objects.equals(attributeName, other.attributeName) && Objects.equals(children, other.children)
+ && Objects.equals(identifier, other.identifier) && isMulti == other.isMulti
+ && Objects.equals(ruleId, other.ruleId);
+ }
+ }
+ public static class FieldValueImpl implements FieldValue {
+ private Field field;
+ private Optional extends Object> value;
+
+ public FieldValueImpl(Field field, Optional extends Object> value) {
+ Objects.requireNonNull(field);
+ Objects.requireNonNull(value);
+ this.field = field;
+ this.value = value;
+ }
+
+ @Override
+ public Field getField() {
+ return field;
+ }
+ @Override
+ public Optional extends Object> getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ return "<" + field.getName() + ", " + value.map(Object::toString).orElse("") + ">";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ FieldValueImpl other = (FieldValueImpl) obj;
+ return Objects.equals(field, other.field) && Objects.equals(value, other.value);
+ }
+ }
+ public static class NestedFieldValueImpl implements NestedFieldValue {
+ private Field field;
+ private Optional extends List extends FieldValue>> value;
+
+ public NestedFieldValueImpl(Field field, Optional extends List extends FieldValue>> value) {
+ Objects.requireNonNull(field);
+ Objects.requireNonNull(value);
+ value.ifPresent(vs -> {
+ Validate.noNullElements(vs);
+ });
+ this.field = field;
+ this.value = value;
+ }
+
+ @Override
+ public Field getField() {
+ return field;
+ }
+ @Override
+ public Optional extends List extends FieldValue>> getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ String valueRepr = value
+ .map(vs -> vs.stream()
+ .map(Object::toString)
+ .collect(Collectors.joining(", ", "{", "}")))
+ .orElse("");
+ return "<" + field.getName() + ", " + valueRepr + ">";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ NestedFieldValueImpl other = (NestedFieldValueImpl) obj;
+ return Objects.equals(field, other.field) && Objects.equals(value, other.value);
+ }
+ }
+ public static class MultiNestedFieldValueImpl implements MultiNestedFieldValue {
+ private Field field;
+ private Optional extends List extends List extends FieldValue>>> value;
+
+ public MultiNestedFieldValueImpl(Field field, Optional extends List extends List extends FieldValue>>> value) {
+ Objects.requireNonNull(field);
+ Objects.requireNonNull(value);
+ value.ifPresent(vs -> {
+ Validate.noNullElements(vs);
+ vs.forEach(v -> Validate.noNullElements(v));
+ });
+ this.field = field;
+ this.value = value;
+ }
+
+ @Override
+ public Field getField() {
+ return field;
+ }
+ @Override
+ public Optional extends List extends List extends FieldValue>>> getValue() {
+ return value;
+ }
+
+ @Override
+ public String toString() {
+ String valueRepr = value
+ .map(vs -> vs.stream()
+ .map(v -> v.stream()
+ .map(Object::toString)
+ .collect(Collectors.joining(", ", "{", "}")))
+ .collect(Collectors.joining(", ", "[", "]")))
+ .orElse("");
+ return "<" + field.getName() + ", " + valueRepr + ">";
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(field, value);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ MultiNestedFieldValueImpl other = (MultiNestedFieldValueImpl) obj;
+ return Objects.equals(field, other.field) && Objects.equals(value, other.value);
+ }
+ }
+}