diff --git a/doc.json b/doc.json deleted file mode 100644 index f786db6..0000000 --- a/doc.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "c": { - "type": "integer" - }, - "a": { - "type": "integer", - "minimum": 0 - }, - "b": { - "type": "integer" - } - }, - "required": [ - "a", - "b" - ], - "additionalProperties": false -} \ No newline at end of file diff --git a/nada_dsl/ast_util.py b/nada_dsl/ast_util.py index b535e3a..c521019 100644 --- a/nada_dsl/ast_util.py +++ b/nada_dsl/ast_util.py @@ -419,3 +419,24 @@ def to_mir(self): "source_ref_index": self.source_ref.to_index(), } } + +@dataclass +class DocumentAccessorASTOperation(ASTOperation): + """AST representation of an document accessor operation.""" + + key: str + source: int + + def child_operations(self): + return [self.source] + + def to_mir(self): + return { + "DocumentAccessor": { + "id": self.id, + "key": self.key, + "source": self.source, + "type": self.ty, + "source_ref_index": self.source_ref.to_index(), + } + } diff --git a/nada_dsl/compiler_frontend.py b/nada_dsl/compiler_frontend.py index de41075..5a52203 100644 --- a/nada_dsl/compiler_frontend.py +++ b/nada_dsl/compiler_frontend.py @@ -11,6 +11,7 @@ from typing import List, Dict, Any, Optional, Tuple from sortedcontainers import SortedDict +from nada_dsl import DocumentAccessorASTOperation from nada_dsl.ast_util import ( AST_OPERATIONS, ASTOperation, @@ -300,6 +301,7 @@ def process_operation( NadaFunctionArgASTOperation, NTupleAccessorASTOperation, ObjectAccessorASTOperation, + DocumentAccessorASTOperation, ), ): processed_operation = ProcessOperationOutput(operation.to_mir(), None) diff --git a/nada_dsl/nada_types/__init__.py b/nada_dsl/nada_types/__init__.py index 1c30396..926ed90 100644 --- a/nada_dsl/nada_types/__init__.py +++ b/nada_dsl/nada_types/__init__.py @@ -114,7 +114,6 @@ def is_numeric(self) -> bool: # TODO: abstract? -@dataclass class DslType: """Nada type class. @@ -136,8 +135,6 @@ class DslType: """ - child: OperationType - def __init__(self, child: OperationType): """NadaType default constructor diff --git a/nada_dsl/nada_types/collections.py b/nada_dsl/nada_types/collections.py index 5b283d8..e1a9d5c 100644 --- a/nada_dsl/nada_types/collections.py +++ b/nada_dsl/nada_types/collections.py @@ -1,5 +1,5 @@ """Nada Collection type definitions.""" - +import os from dataclasses import dataclass import inspect import json @@ -14,7 +14,7 @@ NewASTOperation, ObjectAccessorASTOperation, ReduceASTOperation, - UnaryASTOperation, + UnaryASTOperation, DocumentAccessorASTOperation, ) from nada_dsl.nada_types import DslType @@ -169,7 +169,7 @@ def type(self): return TupleType(self.left_type, self.right_type) -def _generate_accessor(ty: Any, accessor: Any) -> DslType: +def _wrap_accessor_with_type(ty: Any, accessor: Any) -> DslType: if hasattr(ty, "ty") and ty.ty.is_literal(): # TODO: fix raise TypeError("Literals are not supported in accessors") return ty.instantiate(accessor) @@ -180,7 +180,7 @@ class NTupleType(NadaType): is_compound = True - def __init__(self, types: List[DslType]): + def __init__(self, types: List[NadaType]): self.types = types def instantiate(self, child_or_value): @@ -228,7 +228,7 @@ def __getitem__(self, index: int) -> DslType: source_ref=SourceRef.back_frame(), ) - return _generate_accessor(self.types[index], accessor) + return _wrap_accessor_with_type(self.types[index], accessor) def type(self): """Metatype for NTuple""" @@ -270,7 +270,7 @@ class ObjectType(NadaType): is_compound = True - def __init__(self, types: Dict[str, DslType]): + def __init__(self, types: Dict[str, NadaType]): self.types = types def to_mir(self): @@ -318,7 +318,7 @@ def __getattr__(self, attr: str) -> DslType: source_ref=SourceRef.back_frame(), ) - return _generate_accessor(self.types[attr], accessor) + return _wrap_accessor_with_type(self.types[attr], accessor) def type(self): """Metatype for Object""" @@ -354,85 +354,117 @@ def store_in_ast(self, ty: object): ty=ty, ) +class DocumentType(NadaType): + """Marker type for Objects.""" -def _process_schema(schema_node): - if "type" not in schema_node: - raise TypeError("Missing 'type' in schema node") - - match schema_node["type"]: - case "integer": - return PublicInteger(child=None) - case "boolean": - return PublicBoolean(child=None) - case "array": - items_schema = schema_node.get("items") - if items_schema is None: - raise TypeError("Array schema missing 'items'") - ntuple = NTuple.__new__(NTuple) - ntuple.types = [] - ntuple.child = None - for index, item_schema in enumerate(items_schema): - ty = _process_schema(item_schema) - ntuple.types.append(ty) - return ntuple - case "object": - properties = schema_node.get("properties", {}) - obj = Object.__new__(Object) - obj.types = {} - obj.child = None - for key, prop_schema in properties.items(): - ty = _process_schema(prop_schema) - obj.types[key] = ty - return obj - case _: - raise TypeError(f"Unsupported type in schema: {schema_node['type']}") - - -def _assign_accessors(parent): - if isinstance(parent, NTuple): - for index, ty in enumerate(parent.types): - accessor = NTupleAccessor( - index=index, - child=parent, - source_ref=SourceRef.back_frame(), - ) - ty.child = accessor - _assign_accessors(ty) - elif isinstance(parent, Object): - for key, ty in parent.types.items(): - accessor = ObjectAccessor( - key=key, - child=parent, - source_ref=SourceRef.back_frame(), - ) - ty.child = accessor - _assign_accessors(ty) - else: - # Base case: PublicInteger or PublicBoolean - pass + is_compound = True + + def __init__(self, types: Dict[str, NadaType]): + self.types = types + + def to_mir(self): + """Convert an object into a Nada type.""" + return { + "Document": {"types": {name: ty.to_mir() for name, ty in self.types.items()}} + } + def instantiate(self, child_or_value): + return Document(child_or_value, self.types) -class Document(Object): +class Document(DslType): """The Document type""" - def __init__(self, child, filepath: str): - with open(filepath, "r") as schema_file: - schema = json.load(schema_file) + def __init__(self, child, filepath: str = None, public: bool = False, schema: dict = None): + if not schema: + with open(filepath, "r") as schema_file: + schema = json.load(schema_file) try: Draft7Validator.check_schema(schema) except SchemaError as e: raise TypeError("Schema validation error:", e.message) - if schema["type"] != "object": - raise TypeError("Only objects are supported at the root") - # result = _process_schema(self, 0, 0, schema) - # result = _process_schema(schema) - # _assign_accessors(result) + self.__schema = schema + self.__public = public + super().__init__(child) + + def __getattr__(self, item): + if item not in self.__schema["properties"]: + raise AttributeError( + f"'Document has no attribute '{item}'" + ) + + accessor = DocumentAccessor( + key=item, + child=self, + source_ref=SourceRef.back_frame(), + ) + + attribute_type = self.__schema_to_nada_type(self.__schema["properties"][item]) + + return _wrap_accessor_with_type(attribute_type, accessor) + + def __schema_to_nada_type(self, schema: dict) -> NadaType: + IntegerType = PublicIntegerType if self.__public else SecretIntegerType + UnsignedIntegerType = PublicUnsignedIntegerType if self.__public else SecretUnsignedIntegerType + BooleanType = PublicBooleanType if self.__public else SecretBooleanType + if schema["type"] == "integer" and "nada_type" in schema and schema["nada_type"] == "unsigned": + return UnsignedIntegerType() + if schema["type"] == "integer": + return IntegerType() + elif schema["type"] == "boolean": + return BooleanType() + elif schema["type"] == "object": + return ObjectType(types={key: self.__schema_to_nada_type(value) for key, value in schema["properties"].items()}) + elif schema["type"] == "array" and "prefixItems" in schema: + return NTupleType([self.__schema_to_nada_type(value) for value in schema["prefixItems"]]) + elif schema["type"] == "array": + if "size" not in schema: + raise TypeError("size not defined in array schema") + return ArrayType(contained_type=self.__schema_to_nada_type(schema["items"]), size=schema["size"]) + else: + raise TypeError(f"type '{schema['type']}' not supported in json schema") - super().__init__(child=child, types={}) # TMP + def type(self): + return DocumentType({key: self.__schema_to_nada_type(value) for key, value in self.__schema["properties"].items()}) +class PublicDocument(Document): + def __init__(self, child, filepath: str): + super().__init__(child, filepath, True) + +class SecretDocument(Document): + def __init__(self, child, filepath: str): + super().__init__(child, filepath, False) + +@dataclass +class DocumentAccessor: + """Accessor for Object""" + + child: Object + key: str + source_ref: SourceRef + + def __init__( + self, + child: Object, + key: str, + source_ref: SourceRef, + ): + self.id = next_operation_id() + self.child = child + self.key = key + self.source_ref = source_ref + + def store_in_ast(self, ty: object): + """Store this accessor in the AST.""" + AST_OPERATIONS[self.id] = DocumentAccessorASTOperation( + id=self.id, + source=self.child.child.id, + key=self.key, + source_ref=self.source_ref, + ty=ty, + ) class Zip: """The Zip operation.""" diff --git a/test-programs/doc.json b/test-programs/doc.json new file mode 100644 index 0000000..ed9c67e --- /dev/null +++ b/test-programs/doc.json @@ -0,0 +1,40 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "a": { + "type": "integer" + }, + "b": { + "type": "boolean" + }, + "c": { + "type": "object", + "properties": { + "c": { + "type": "integer" + } + } + }, + "d": { + "type": "array", + "items": { + "type": "integer" + }, + "size": 2 + }, + "e": { + "type": "array", + "prefixItems": [{"type": "integer"}, {"type": "boolean"}] + }, + "f": { + "type": "integer", + "nada_type": "unsigned_integer" + } + }, + "required": [ + "a", + "b" + ], + "additionalProperties": false +} \ No newline at end of file diff --git a/test-programs/document.py b/test-programs/document.py index 19d17a4..ee594c1 100644 --- a/test-programs/document.py +++ b/test-programs/document.py @@ -1,9 +1,55 @@ +import os.path + from nada_dsl import * +schema = { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "a": { + "type": "integer" + }, + "b": { + "type": "boolean" + }, + "c": { + "type": "object", + "properties": { + "c": { + "type": "integer" + } + } + }, + "d": { + "type": "array", + "items": { + "type": "integer" + }, + "size": 2 + }, + "e": { + "type": "array", + "prefixItems": [{"type": "integer"}, {"type": "boolean"}] + }, + "f": { + "type": "integer", + "nada_type": "unsigned_integer" + } + }, + "required": [ + "a", + "b" + ], + "additionalProperties": False +} def nada_main(): party1 = Party(name="Party1") - doc = Document(Input(name="my_doc", party=party1), "doc.json") + doc = Document(Input(name="my_doc", party=party1), schema=schema) my_int = PublicInteger(Input(name="my_int", party=party1)) - + doc.a + doc.b + doc.c + doc.d + doc.e return [Output(doc.a + my_int, "my_output", party1)] diff --git a/test-programs/mir.json b/test-programs/mir.json new file mode 100644 index 0000000..bb6e09c --- /dev/null +++ b/test-programs/mir.json @@ -0,0 +1,164 @@ +{ + "functions": [], + "parties": [ + { + "name": "Party1", + "source_ref_index": 4 + } + ], + "inputs": [ + { + "name": "my_int", + "type": "Integer", + "party": "Party1", + "doc": "", + "source_ref_index": 1 + }, + { + "name": "my_doc", + "type": { + "Document": { + "types": { + "a": "SecretInteger", + "b": "SecretBoolean", + "c": { + "Object": { + "types": { + "c": "SecretInteger" + } + } + }, + "d": { + "Array": { + "inner_type": "SecretInteger", + "size": 2 + } + }, + "e": { + "NTuple": { + "types": [ + "SecretInteger", + "SecretBoolean" + ] + } + }, + "f": "SecretInteger" + } + } + }, + "party": "Party1", + "doc": "", + "source_ref_index": 3 + } + ], + "literals": [], + "outputs": [ + { + "operation_id": 9, + "name": "my_output", + "party": "Party1", + "type": "SecretInteger", + "source_ref_index": 2 + } + ], + "operations": { + "9": { + "Addition": { + "id": 9, + "left": 8, + "right": 2, + "type": "SecretInteger", + "source_ref_index": 0 + } + }, + "2": { + "InputReference": { + "id": 2, + "refers_to": "my_int", + "type": "Integer", + "source_ref_index": 1 + } + }, + "8": { + "DocumentAccessor": { + "id": 8, + "key": "a", + "source": 1, + "type": "SecretInteger", + "source_ref_index": 2 + } + }, + "1": { + "InputReference": { + "id": 1, + "refers_to": "my_doc", + "type": { + "Document": { + "types": { + "a": "SecretInteger", + "b": "SecretBoolean", + "c": { + "Object": { + "types": { + "c": "SecretInteger" + } + } + }, + "d": { + "Array": { + "inner_type": "SecretInteger", + "size": 2 + } + }, + "e": { + "NTuple": { + "types": [ + "SecretInteger", + "SecretBoolean" + ] + } + }, + "f": "SecretInteger" + } + } + }, + "source_ref_index": 3 + } + } + }, + "source_files": { + "document.py": "import os.path\n\nfrom nada_dsl import *\n\nschema = {\n \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n \"type\": \"object\",\n \"properties\": {\n \"a\": {\n \"type\": \"integer\"\n },\n \"b\": {\n \"type\": \"boolean\"\n },\n \"c\": {\n \"type\": \"object\",\n \"properties\": {\n \"c\": {\n \"type\": \"integer\"\n }\n }\n },\n \"d\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"integer\"\n },\n \"size\": 2\n },\n \"e\": {\n \"type\": \"array\",\n \"prefixItems\": [{\"type\": \"integer\"}, {\"type\": \"boolean\"}]\n },\n \"f\": {\n \"type\": \"integer\",\n \"nada_type\": \"unsigned_integer\"\n }\n },\n \"required\": [\n \"a\",\n \"b\"\n ],\n \"additionalProperties\": False\n}\n\ndef nada_main():\n party1 = Party(name=\"Party1\")\n doc = Document(Input(name=\"my_doc\", party=party1), schema=schema)\n my_int = PublicInteger(Input(name=\"my_int\", party=party1))\n doc.a\n doc.b\n doc.c\n doc.d\n doc.e\n return [Output(doc.a + my_int, \"my_output\", party1)]\n" + }, + "source_refs": [ + { + "lineno": 137, + "offset": 0, + "file": "scalar_types.py", + "length": 0 + }, + { + "lineno": 49, + "offset": 1031, + "file": "document.py", + "length": 62 + }, + { + "lineno": 55, + "offset": 0, + "file": "document.py", + "length": 0 + }, + { + "lineno": 48, + "offset": 961, + "file": "document.py", + "length": 69 + }, + { + "lineno": 47, + "offset": 927, + "file": "document.py", + "length": 33 + } + ] +} diff --git a/tests/compile_test.py b/tests/compile_test.py index 4825111..ce381dc 100644 --- a/tests/compile_test.py +++ b/tests/compile_test.py @@ -157,3 +157,4 @@ def test_compile_document(): mir_str = compile_script(f"{get_test_programs_folder()}/document.py").mir print(f"{mir_str}") assert mir_str != "" + raise Exception("TODO: implement the rest of the test")