X-Git-Url: http://nitlanguage.org diff --git a/src/frontend/serialization_phase.nit b/src/frontend/serialization_phase.nit index 4e1ce6c..1091c47 100644 --- a/src/frontend/serialization_phase.nit +++ b/src/frontend/serialization_phase.nit @@ -22,16 +22,20 @@ module serialization_phase private import parser_util import modelize private import annotation +intrude import literal redef class ToolContext + + # Apply the annotation `serialize_as` + var serialization_phase_rename: Phase = new SerializationPhaseRename(self, null) + # Generate serialization and deserialization methods on `auto_serializable` annotated classes. - var serialization_phase_pre_model: Phase = new SerializationPhasePreModel(self, null) + var serialization_phase_pre_model: Phase = new SerializationPhasePreModel(self, + [serialization_phase_rename]) # The second phase of the serialization var serialization_phase_post_model: Phase = new SerializationPhasePostModel(self, - [modelize_class_phase, serialization_phase_pre_model]) - - private fun place_holder_type_name: String do return "PlaceHolderTypeWhichShouldNotExist" + [modelize_property_phase, serialization_phase_pre_model]) end redef class ANode @@ -40,8 +44,6 @@ redef class ANode # Is this node annotated to not be made serializable? private fun is_noserialize: Bool do return false - - private fun accept_precise_type_visitor(v: PreciseTypeVisitor) do visit_all(v) end redef class ADefinition @@ -56,7 +58,34 @@ redef class ADefinition end end -# TODO add annotations on attributes (volatile, sensitive or do_not_serialize?) +private class SerializationPhaseRename + super Phase + + redef fun process_annotated_node(node, nat) + do + var text = nat.n_atid.n_id.text + if text != "serialize_as" then return + + if not node isa AAttrPropdef then + toolcontext.error(node.location, + "Syntax Error: annotation `{text}` applies only to attributes.") + return + end + + # Can't use `arg_as_string` because it needs the literal phase + var args = nat.n_args + if args.length != 1 or not args.first isa AStringFormExpr then + toolcontext.error(node.location, + "Syntax Error: annotation `{text}` expects a single string literal as argument.") + return + end + + var t = args.first.collect_text + var val = t.substring(1, t.length-2) + node.serialize_name = val + end +end + private class SerializationPhasePreModel super Phase @@ -89,7 +118,7 @@ private class SerializationPhasePreModel if not node isa AModuledecl then var up_serialize = false var up: nullable ANode = node - loop + while up != null do up = up.parent if up == null then break @@ -116,35 +145,19 @@ private class SerializationPhasePreModel do if not nclassdef isa AStdClassdef then return - # Is there a declaration on the classdef or the module? - var serialize = nclassdef.is_serialize + var serialize_by_default = nclassdef.how_serialize - if not serialize and not nclassdef.is_noserialize then - # Is the module marked serialize? - serialize = nclassdef.parent.as(AModule).is_serialize - end + if serialize_by_default != null then - var per_attribute = false - if not serialize then - # Is there an attribute marked serialize? - for npropdef in nclassdef.n_propdefs do - if npropdef.is_serialize then - serialize = true - per_attribute = true - break - end - end - end - - if serialize then # Add `super Serializable` var sc = toolcontext.parse_superclass("Serializable") sc.location = nclassdef.location nclassdef.n_propdefs.add sc # Add services + var per_attribute = not serialize_by_default generate_serialization_method(nclassdef, per_attribute) - generate_deserialization_init(nclassdef, per_attribute) + generate_deserialization_init(nclassdef) end end @@ -154,22 +167,30 @@ private class SerializationPhasePreModel nmodule.inits_to_retype.clear # collect all classes - var auto_serializable_nclassdefs = new Array[AStdClassdef] - for nclassdef in nmodule.n_classdefs do - if nclassdef isa AStdClassdef and nclassdef.is_serialize then - auto_serializable_nclassdefs.add nclassdef - end - end - + var auto_serializable_nclassdefs = nmodule.auto_serializable_nclassdefs if not auto_serializable_nclassdefs.is_empty then generate_deserialization_method(nmodule, auto_serializable_nclassdefs) end end + # Implement `core_serialize_to` on `nclassdef` + # + # Are attributes serialized on demand `per_attribute` with `serialize`? + # Otherwise they are serialized by default, and we check instead for `noserialize`. fun generate_serialization_method(nclassdef: AClassdef, per_attribute: Bool) do var npropdefs = nclassdef.n_propdefs + # Do not insert a `core_serialize_to` if it already exists + for npropdef in npropdefs do + if npropdef isa AMethPropdef then + var methid = npropdef.n_methid + if methid != null and methid.collect_text == "core_serialize_to" then + return + end + end + end + var code = new Array[String] code.add "redef fun core_serialize_to(v)" code.add "do" @@ -181,8 +202,7 @@ private class SerializationPhasePreModel if (per_attribute and not attribute.is_serialize) or attribute.is_noserialize then continue - var name = attribute.name - code.add " v.serialize_attribute(\"{name}\", {name})" + code.add " v.serialize_attribute(\"{attribute.serialize_name}\", {attribute.name})" end code.add "end" @@ -191,58 +211,34 @@ private class SerializationPhasePreModel npropdefs.push(toolcontext.parse_propdef(code.join("\n"))) end - # Add a constructor to the automated nclassdef - fun generate_deserialization_init(nclassdef: AClassdef, per_attribute: Bool) + # Add an empty constructor to the automated nclassdef + # + # Will be filled by `SerializationPhasePostModel`. + fun generate_deserialization_init(nclassdef: AClassdef) do var npropdefs = nclassdef.n_propdefs - var code = new Array[String] - code.add """ -redef init from_deserializer(v: Deserializer) -do - super - v.notify_of_creation self -""" - - for attribute in npropdefs do if attribute isa AAttrPropdef then - - # Is `attribute` to be skipped? - if (per_attribute and not attribute.is_serialize) or - attribute.is_noserialize then continue - - var n_type = attribute.n_type - var type_name - if n_type == null then - # Use a place holder, we will replace it with the inferred type after the model phases - type_name = toolcontext.place_holder_type_name - else - type_name = n_type.type_name + # Do not insert a `from_deserializer` if it already exists + for npropdef in npropdefs do + if npropdef isa AMethPropdef then + var methid = npropdef.n_methid + if methid != null and methid.collect_text == "from_deserializer" then + return + end end - var name = attribute.name - - code.add """ - var {{{name}}} = v.deserialize_attribute("{{{name}}}") - if not {{{name}}} isa {{{type_name}}} then - # Check if it was a subjectent error - v.errors.add new AttributeTypeError("TODO remove this arg on c_src regen", - self, "{{{name}}}", {{{name}}}, "{{{type_name}}}") - - # Clear subjacent error - if v.keep_going == false then return - else - self.{{{name}}} = {{{name}}} - end -""" end - code.add "end" + var code = """ +redef init from_deserializer(v: Deserializer) do abort""" - var npropdef = toolcontext.parse_propdef(code.join("\n")).as(AMethPropdef) + var npropdef = toolcontext.parse_propdef(code).as(AMethPropdef) npropdefs.add npropdef nclassdef.parent.as(AModule).inits_to_retype.add npropdef end - # Added to the abstract serialization service + # Add an empty `Deserializer::deserialize_class_intern` + # + # Will be filled by `SerializationPhasePostModel`. fun generate_deserialization_method(nmodule: AModule, nclassdefs: Array[AStdClassdef]) do var code = new Array[String] @@ -259,25 +255,12 @@ do if deserializer_npropdef == null then # create the property - code.add " redef fun deserialize_class(name)" - code.add " do" + code.add " redef fun deserialize_class_intern(name) do abort" else - toolcontext.error(deserializer_npropdef.location, "Error: you cannot define `Deserializer::deserialize_class` in a module where you use `auto_serializable`.") + toolcontext.error(deserializer_npropdef.location, "Error: `Deserializer::deserialize_class_intern` is generated and must not be defined, use `deserialize_class` instead.") return end - for nclassdef in nclassdefs do - var name = nclassdef.n_id.text - if nclassdef.n_formaldefs.is_empty and - not nclassdef.n_classkind isa AAbstractClasskind then - - code.add " if name == \"{name}\" then return new {name}.from_deserializer(self)" - end - end - - code.add " return super" - code.add " end" - if deserializer_nclassdef == null then code.add "end" nmodule.n_classdefs.add toolcontext.parse_classdef(code.join("\n")) @@ -290,55 +273,197 @@ end private class SerializationPhasePostModel super Phase + # Fill the deserialization init `from_deserializer` and `Deserializer.deserialize_class_intern` redef fun process_nmodule(nmodule) do for npropdef in nmodule.inits_to_retype do - var mpropdef = npropdef.mpropdef - if mpropdef == null then continue # skip error - var v = new PreciseTypeVisitor(npropdef, mpropdef.mclassdef, toolcontext) - npropdef.accept_precise_type_visitor v + var nclassdef = npropdef.parent + assert nclassdef isa AStdClassdef + + var serialize_by_default = nclassdef.how_serialize + assert serialize_by_default != null + + var per_attribute = not serialize_by_default + fill_deserialization_init(nclassdef, npropdef, per_attribute) + end + + # collect all classes + var auto_serializable_nclassdefs = nmodule.auto_serializable_nclassdefs + if not auto_serializable_nclassdefs.is_empty then + fill_deserialization_method(nmodule, auto_serializable_nclassdefs) end end -end -# Visitor on generated constructors to replace the expected type of deserialized attributes -private class PreciseTypeVisitor - super Visitor + # Fill the constructor to the generated `init_npropdef` of `nclassdef` + fun fill_deserialization_init(nclassdef: AClassdef, init_npropdef: AMethPropdef, per_attribute: Bool) + do + var code = new Array[String] + code.add """ +redef init from_deserializer(v: Deserializer) +do + super + v.notify_of_creation self +""" - var npropdef: AMethPropdef - var mclassdef: MClassDef - var toolcontext: ToolContext + for attribute in nclassdef.n_propdefs do + if not attribute isa AAttrPropdef then continue - redef fun visit(n) do n.accept_precise_type_visitor(self) -end + # Is `attribute` to be skipped? + if (per_attribute and not attribute.is_serialize) or + attribute.is_noserialize then continue + + var mtype = attribute.mtype + if mtype == null then continue + var type_name = mtype.to_s + var name = attribute.name + + var resolved_type_name = type_name + var mclassdef = nclassdef.mclassdef + if mclassdef != null then + var bound_mtype = mclassdef.bound_mtype + var resolved_mtype = mtype.resolve_for(bound_mtype, bound_mtype, mclassdef.mmodule, true) + resolved_type_name = resolved_mtype.name + + # TODO Use something like `V.class_name` to get the precise runtime type of virtual types. + # We currently use the upper bound of virtual types as static type in generated code + # for type suggestion and to prevent loading unexected types. + # This leaves a security issue when, for example, `DefaultMap::default_value` + # is bound to `nullable Object` and would allow any object to be loaded. + end + + if type_name == "nullable Object" then + # Don't type check + code.add """ + self.{{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{resolved_type_name}}}") +""" + else + code.add """ + var {{{name}}} = v.deserialize_attribute("{{{attribute.serialize_name}}}", "{{{resolved_type_name}}}") + if v.deserialize_attribute_missing then +""" + # What to do when an attribute is missing? + if attribute.has_value then + # Leave it to the default value + else if mtype isa MNullableType then + code.add """ + self.{{{name}}} = null""" + else code.add """ + v.errors.add new Error("Deserialization Error: attribute `{class_name}::{{{name}}}` missing from JSON object")""" + + code.add """ + else if not {{{name}}} isa {{{type_name}}} then + v.errors.add new AttributeTypeError(self, "{{{attribute.serialize_name}}}", {{{name}}}, "{{{resolved_type_name}}}") + if v.keep_going == false then return + else + self.{{{name}}} = {{{name}}} + end +""" + end + end + + code.add "end" -redef class AIsaExpr - redef fun accept_precise_type_visitor(v) + # Replace the body of the constructor + var npropdef = toolcontext.parse_propdef(code.join("\n")).as(AMethPropdef) + init_npropdef.n_block = npropdef.n_block + + # Run the literal phase on the generated code + var v = new LiteralVisitor(toolcontext) + v.enter_visit(npropdef.n_block) + end + + # Fill the abstract serialization service + fun fill_deserialization_method(nmodule: AModule, nclassdefs: Array[AStdClassdef]) do - if n_type.collect_text != v.toolcontext.place_holder_type_name then return - - var attr_name = "_" + n_expr.collect_text - for mattrdef in v.mclassdef.mpropdefs do - if mattrdef isa MAttributeDef and mattrdef.name == attr_name then - var new_ntype = v.toolcontext.parse_something(mattrdef.static_mtype.to_s) - n_type.replace_with new_ntype - break + var deserializer_nclassdef = nmodule.deserializer_nclassdef + if deserializer_nclassdef == null then return + var deserializer_npropdef = deserializer_nclassdef.deserializer_npropdef + if deserializer_npropdef == null then return + + # Collect local types expected to be deserialized + var types_to_deserialize = new Set[String] + + ## Local serializable standard class without parameters + for nclassdef in nclassdefs do + var mclass = nclassdef.mclass + if mclass == null then continue + + if mclass.arity == 0 and mclass.kind == concrete_kind then + types_to_deserialize.add mclass.name + end + end + + ## Static parametized types on serializable attributes + for nclassdef in nmodule.n_classdefs do + if not nclassdef isa AStdClassdef then continue + + for attribute in nclassdef.n_propdefs do + if not attribute isa AAttrPropdef then continue + + var serialize_by_default = nclassdef.how_serialize + if serialize_by_default == null then continue + var per_attribute = not serialize_by_default + + # Is `attribute` to be skipped? + if (per_attribute and not attribute.is_serialize) or + attribute.is_noserialize then continue + + var mtype = attribute.mtype + if mtype == null then continue + if mtype isa MNullableType then mtype = mtype.mtype + + if mtype isa MClassType and mtype.mclass.arity > 0 and + mtype.mclass.kind == concrete_kind and not mtype.need_anchor then + + # Check is a `Serializable` + var mmodule = nmodule.mmodule + if mmodule == null then continue + + var greaters = mtype.mclass.in_hierarchy(mmodule).greaters + var is_serializable = false + for sup in greaters do if sup.name == "Serializable" then + is_serializable = true + break + end + + if is_serializable then types_to_deserialize.add mtype.to_s + end end end + + # Build implementation code + var code = new Array[String] + code.add "redef fun deserialize_class_intern(name)" + code.add "do" + + for name in types_to_deserialize do + code.add " if name == \"{name}\" then return new {name}.from_deserializer(self)" + end + + code.add " return super" + code.add "end" + + # Replace the body of the constructor + var npropdef = toolcontext.parse_propdef(code.join("\n")).as(AMethPropdef) + deserializer_npropdef.n_block = npropdef.n_block + + # Run the literal phase on the generated code + var v = new LiteralVisitor(toolcontext) + v.enter_visit(npropdef.n_block) end end redef class AAttrPropdef - private fun name: String - do - return n_id2.text - end + private fun name: String do return n_id2.text + + # Name of this attribute in the serialized format + private var serialize_name: String = name is lazy end redef class AType private fun type_name: String do - var name = n_id.text + var name = n_qid.n_id.text if n_kwnullable != null then name = "nullable {name}" @@ -355,9 +480,9 @@ redef class AModule private fun deserializer_nclassdef: nullable AStdClassdef do for nclassdef in n_classdefs do - if nclassdef isa AStdClassdef and nclassdef.n_id.text == "Deserializer" then - return nclassdef - end + if not nclassdef isa AStdClassdef then continue + var n_qid = nclassdef.n_qid + if n_qid != null and n_qid.n_id.text == "Deserializer" then return nclassdef end return null @@ -365,7 +490,22 @@ redef class AModule private var inits_to_retype = new Array[AMethPropdef] - redef fun is_serialize do return n_moduledecl != null and n_moduledecl.is_serialize + redef fun is_serialize + do + var n_moduledecl = n_moduledecl + return n_moduledecl != null and n_moduledecl.is_serialize + end + + # `AStdClassdef` marked as serializable, itself or one of theur attribute + private var auto_serializable_nclassdefs: Array[AStdClassdef] is lazy do + var array = new Array[AStdClassdef] + for nclassdef in n_classdefs do + if nclassdef isa AStdClassdef and nclassdef.how_serialize != null then + array.add nclassdef + end + end + return array + end end redef class AStdClassdef @@ -373,11 +513,41 @@ redef class AStdClassdef do for npropdef in n_propdefs do if npropdef isa AMethPropdef then var id = npropdef.n_methid - if id isa AIdMethid and id.n_id.text == "deserialize_class" then + if id isa AIdMethid and id.n_id.text == "deserialize_class_intern" then return npropdef end end return null end + + # Is this classed marked `serialize`? in part or fully? + # + # This method returns 3 possible values: + # * `null`, this class is not to be serialized. + # * `true`, the attributes of this class are to be serialized by default. + # * `false`, the attributes of this class are to be serialized on demand only. + fun how_serialize: nullable Bool + do + # Is there a declaration on the classdef or the module? + var serialize = is_serialize + + if not serialize and not is_noserialize then + # Is the module marked serialize? + serialize = parent.as(AModule).is_serialize + end + + if serialize then return true + + if not serialize then + # Is there an attribute marked serialize? + for npropdef in n_propdefs do + if npropdef.is_serialize then + return false + end + end + end + + return null + end end