| Index: lib/compiler/implementation/typechecker.dart
|
| diff --git a/lib/compiler/implementation/typechecker.dart b/lib/compiler/implementation/typechecker.dart
|
| index f5a600fa3e08f04b40f497efe977c7630b43dbe0..41750c9bb50e95d63a9261f4dc4635208c0fe900 100644
|
| --- a/lib/compiler/implementation/typechecker.dart
|
| +++ b/lib/compiler/implementation/typechecker.dart
|
| @@ -27,14 +27,92 @@ class TypeCheckerTask extends CompilerTask {
|
| interface Type {
|
| SourceString get name();
|
| Element get element();
|
| +
|
| + /**
|
| + * Performs the substitution `[arguments[i]/parameters[i]]this`.
|
| + * The notation is known from this lambda calculus rule:
|
| + *
|
| + * (lambda x.e0)e1 -> [e1/x]e0.
|
| + *
|
| + * See [TypeVariableType] for a motivation for this method.
|
| + */
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters);
|
| +
|
| + /**
|
| + * Returns the defining type for this type. For non-typedef types, the
|
| + * defining type is the type itself, whereas for typedef types the defining
|
| + * type is the defining type of its definition. For example the defining type
|
| + * for `typedef A Func<A,B>(B b)` is the function type `(B) -> A` and the
|
| + * defining type for `Func<int,String>` is the function type
|
| + * `(String) -> int`.
|
| + */
|
| + Type unalias(Compiler compiler);
|
| }
|
|
|
| +/**
|
| + * Represents a type variable, that is the type parameters of a class
|
| + * or interface type. For example, in class Array<E> { ... }`,
|
| + * E is a type variable.
|
| + *
|
| + * Each class/interface should have its own unique type variables,
|
| + * one for each type parameter. A class/interface with type parameters
|
| + * is said to be parameterized or generic.
|
| + *
|
| + * Non-static members, constructors, and factories of generic
|
| + * class/interface can refer to type variables of the current class
|
| + * (not of supertypes).
|
| + *
|
| + * When using a generic type, also known as an application or
|
| + * instantiation of the type, the actual type arguments should be
|
| + * substituted for the type variables in the class declaration.
|
| + *
|
| + * For example, given a box, `class Box<T> { T value; }`, the
|
| + * type of the expression `new Box<String>().value` is
|
| + * [String] because we must substitute `String` for the
|
| + * the type variable `T`.
|
| + */
|
| class TypeVariableType implements Type {
|
| final SourceString name;
|
| TypeVariableElement element;
|
| TypeVariableType(this.name, [this.element]);
|
|
|
| - toString() => name.slowToString();
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + if (parameters.isEmpty()) {
|
| + // Return fast on empty substitutions.
|
| + return this;
|
| + }
|
| + Link<TypeVariableType> parameterLink = parameters;
|
| + if (arguments.isEmpty()) {
|
| + // Raw substitution: Dynamic replaces parameters.
|
| + while (!parameterLink.isEmpty()) {
|
| + TypeVariableType parameter = parameterLink.head;
|
| + if (parameter == this) {
|
| + return compiler.types.dynamicType;
|
| + }
|
| + parameterLink = parameterLink.tail;
|
| + }
|
| + } else {
|
| + // Normal substitution.
|
| + Link<Type> argumentLink = arguments;
|
| + while (!argumentLink.isEmpty() && !parameterLink.isEmpty()) {
|
| + TypeVariableType parameter = parameterLink.head;
|
| + Type argument = argumentLink.head;
|
| + if (parameter == this) {
|
| + return argument;
|
| + }
|
| + parameterLink = parameterLink.tail;
|
| + argumentLink = argumentLink.tail;
|
| + }
|
| + }
|
| + // The type variable was not substituted.
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) => this;
|
| +
|
| + String toString() => name.slowToString();
|
| }
|
|
|
| /**
|
| @@ -57,6 +135,14 @@ class StatementType implements Type {
|
| return (this === other) ? this : MAYBE_RETURNING;
|
| }
|
|
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + // Statement types are not substitutable.
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) => this;
|
| +
|
| String toString() => stringName;
|
| }
|
|
|
| @@ -65,24 +151,96 @@ class VoidType implements Type {
|
| SourceString get name() => element.name;
|
| final VoidElement element;
|
|
|
| - toString() => name.slowToString();
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + // Void cannot be substituted.
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) => this;
|
| +
|
| + String toString() => name.slowToString();
|
| }
|
|
|
| class InterfaceType implements Type {
|
| - final Element element;
|
| - final Link<Type> arguments;
|
| + final ClassElement element;
|
| + final Link<Type> typeArguments;
|
|
|
| const InterfaceType(this.element,
|
| - [this.arguments = const EmptyLink<Type>()]);
|
| + [this.typeArguments = const EmptyLink<Type>()]);
|
|
|
| SourceString get name() => element.name;
|
|
|
| - toString() {
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + if (typeArguments.isEmpty()) {
|
| + // Return fast on non-generic types.
|
| + return this;
|
| + }
|
| + if (parameters.isEmpty()) {
|
| + // Return fast on empty substitutions.
|
| + return this;
|
| + }
|
| + if (arguments.isEmpty()) {
|
| + // Return the 'raw' type on raw substitutions.
|
| + return new InterfaceType(element);
|
| + }
|
| + bool changed = false;
|
| + var argumentsBuilder = new LinkBuilder<Type>();
|
| + Link<Type> typeArgument = typeArguments;
|
| + while (!typeArgument.isEmpty()) {
|
| + var argument =
|
| + typeArgument.head.subst(compiler, arguments, parameters);
|
| + if (!changed && argument !== typeArgument.head) {
|
| + changed = true;
|
| + }
|
| + argumentsBuilder.addLast(argument);
|
| + typeArgument = typeArgument.tail;
|
| + }
|
| + if (changed) {
|
| + // Create a new type only if necessary.
|
| + return new InterfaceType(element, argumentsBuilder.toLink());
|
| + }
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) => this;
|
| +
|
| + /**
|
| + * Finds the method, field or property on this interface type named [name].
|
| + */
|
| + Member lookupMember(Compiler compiler, SourceString name) {
|
| + ClassElement classElement = element;
|
| + InterfaceType receiver = this;
|
| + InterfaceType declarer = receiver;
|
| + Element member = classElement.lookupLocalMember(name);
|
| + if (member === null) {
|
| + classElement.ensureResolved(compiler);
|
| + for (Link<InterfaceType> supertypes = classElement.allSupertypes;
|
| + !supertypes.isEmpty() && member === null;
|
| + supertypes = supertypes.tail) {
|
| + declarer = supertypes.head;
|
| + ClassElement lookupTarget = declarer.element;
|
| + member = lookupTarget.lookupLocalMember(name);
|
| + }
|
| + }
|
| + if (member == null) {
|
| + return null;
|
| + }
|
| + if (member.kind == ElementKind.FUNCTION ||
|
| + member.kind == ElementKind.ABSTRACT_FIELD ||
|
| + member.kind == ElementKind.FIELD) {
|
| + return new Member(receiver, declarer, member);
|
| + }
|
| + return null;
|
| + }
|
| +
|
| + String toString() {
|
| StringBuffer sb = new StringBuffer();
|
| sb.add(name.slowToString());
|
| - if (!arguments.isEmpty()) {
|
| + if (!typeArguments.isEmpty()) {
|
| sb.add('<');
|
| - arguments.printOn(sb, ', ');
|
| + typeArguments.printOn(sb, ', ');
|
| sb.add('>');
|
| }
|
| return sb.toString();
|
| @@ -92,12 +250,41 @@ class InterfaceType implements Type {
|
| class FunctionType implements Type {
|
| final Element element;
|
| final Type returnType;
|
| - final Link<Type> parameterTypes;
|
| + Link<Type> parameterTypes;
|
|
|
| - const FunctionType(Type this.returnType, Link<Type> this.parameterTypes,
|
| - Element this.element);
|
| + FunctionType(Type this.returnType, Link<Type> this.parameterTypes,
|
| + Element this.element);
|
|
|
| - toString() {
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + if (parameters.isEmpty()) {
|
| + // Return fast on empty substitutions.
|
| + return this;
|
| + }
|
| + var newReturnType = returnType.subst(compiler, arguments, parameters);
|
| + bool changed = newReturnType !== returnType;
|
| + var newParameterTypesBuilder = new LinkBuilder<Type>();
|
| + Link<Type> parameterType = parameterTypes;
|
| + while (!parameterType.isEmpty()) {
|
| + var newParameterType =
|
| + parameterType.head.subst(compiler, arguments, parameters);
|
| + if (!changed && newParameterType !== parameterType.head) {
|
| + changed = true;
|
| + }
|
| + newParameterTypesBuilder.addLast(newParameterType);
|
| + parameterType = parameterType.tail;
|
| + }
|
| + if (changed) {
|
| + // Create a new type only if necessary.
|
| + return new FunctionType(newReturnType, newParameterTypesBuilder.toLink(),
|
| + element);
|
| + }
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) => this;
|
| +
|
| + String toString() {
|
| StringBuffer sb = new StringBuffer();
|
| bool first = true;
|
| sb.add('(');
|
| @@ -115,23 +302,151 @@ class FunctionType implements Type {
|
| }
|
| }
|
|
|
| +class TypedefType implements Type {
|
| + final TypedefElement element;
|
| + final Link<Type> typeArguments;
|
| +
|
| + const TypedefType(this.element,
|
| + [this.typeArguments = const EmptyLink<Type>()]);
|
| +
|
| + SourceString get name() => element.name;
|
| +
|
| + Type subst(Compiler compiler,
|
| + Link<Type> arguments, Link<TypeVariableType> parameters) {
|
| + if (typeArguments.isEmpty()) {
|
| + // Return fast on non-generic typedefs.
|
| + return this;
|
| + }
|
| + if (parameters.isEmpty()) {
|
| + // Return fast on empty substitutions.
|
| + return this;
|
| + }
|
| + if (arguments.isEmpty()) {
|
| + // Return the 'raw' type on raw substitutions.
|
| + return new TypedefType(element);
|
| + }
|
| + bool changed = false;
|
| + var argumentsBuilder = new LinkBuilder<Type>();
|
| + Link<Type> typeArgument = typeArguments;
|
| + while (!typeArgument.isEmpty()) {
|
| + var argument =
|
| + typeArgument.head.subst(compiler, arguments, parameters);
|
| + if (!changed && argument !== typeArgument.head) {
|
| + changed = true;
|
| + }
|
| + argumentsBuilder.addLast(argument);
|
| + typeArgument = typeArgument.tail;
|
| + }
|
| + if (changed) {
|
| + // Create a new type only if necessary.
|
| + return new TypedefType(element, argumentsBuilder.toLink());
|
| + }
|
| + return this;
|
| + }
|
| +
|
| + Type unalias(Compiler compiler) {
|
| + compiler.resolveTypedef(element);
|
| + Type definition = element.alias.unalias(compiler);
|
| + TypedefType declaration = element.computeType(compiler);
|
| + return definition.subst(compiler, typeArguments, declaration.typeArguments);
|
| + }
|
| +
|
| + String toString() {
|
| + StringBuffer sb = new StringBuffer();
|
| + sb.add(name.slowToString());
|
| + if (!typeArguments.isEmpty()) {
|
| + sb.add('<');
|
| + typeArguments.printOn(sb, ', ');
|
| + sb.add('>');
|
| + }
|
| + return sb.toString();
|
| + }
|
| +}
|
| +
|
| +/**
|
| + * Member encapsulates a member (constructor, method, field, property, getter,
|
| + * or setter) with the types of the declarer and receiver in order to do
|
| + * substitution on the member type.
|
| + *
|
| + * Consider for instance these classes and the variable `B<String> b`:
|
| + *
|
| + * class A<E> {
|
| + * E field;
|
| + * }
|
| + * class B<F> extends A<F> {}
|
| + *
|
| + * In a [Member] for `b.field` the [receiver] is `B<String>` and the declarer
|
| + * is `A<F>`, which is the supertype of `B<F>` from which `field` has been
|
| + * inherited. To compute the type of `b.field` we must first substitute `E`
|
| + * by `F` using the relation between `A<E>` and `A<F>`, and then `F` by `String`
|
| + * using the relation between `B<F>` and `B<String>`.
|
| + */
|
| +class Member {
|
| + final InterfaceType receiver;
|
| + final InterfaceType declarer;
|
| + final Element element;
|
| +
|
| + Member(this.receiver, this.declarer, this.element);
|
| +
|
| + Type computeType(Compiler compiler) {
|
| + Type type;
|
| + if (element.kind == ElementKind.ABSTRACT_FIELD) {
|
| + AbstractFieldElement abstractFieldElement = element;
|
| + if (abstractFieldElement.getter != null) {
|
| + type = abstractFieldElement.getter.computeType(compiler).returnType;
|
| + } else {
|
| + type = abstractFieldElement.setter.computeType(
|
| + compiler).parameterTypes.head;
|
| + if (type == null) {
|
| + type = compiler.types.dynamicType;
|
| + }
|
| + }
|
| + } else {
|
| + type = element.computeType(compiler);
|
| + }
|
| + if (!declarer.element.typeVariables.isEmpty()) {
|
| + type = type.subst(compiler,
|
| + declarer.typeArguments,
|
| + declarer.element.computeType(compiler).typeArguments);
|
| + type = type.subst(compiler,
|
| + receiver.typeArguments,
|
| + receiver.element.computeType(compiler).typeArguments);
|
| + }
|
| + return type;
|
| + }
|
| +
|
| + String toString() {
|
| + return '$receiver.$element';
|
| + }
|
| +}
|
| +
|
| class Types {
|
| + final Compiler compiler;
|
| final VoidType voidType;
|
| final InterfaceType dynamicType;
|
|
|
| - Types(Element dynamicElement)
|
| - : this.with(dynamicElement, new LibraryElement(new Script(null, null)));
|
| + Types(Compiler compiler, Element dynamicElement)
|
| + : this.with(compiler, dynamicElement,
|
| + new LibraryElement(new Script(null, null)));
|
|
|
| // TODO(karlklose): should we have a class Void?
|
| - Types.with(Element dynamicElement, LibraryElement library)
|
| + Types.with(Compiler this.compiler,
|
| + Element dynamicElement,
|
| + LibraryElement library)
|
| : voidType = new VoidType(new VoidElement(library)),
|
| dynamicType = new InterfaceType(dynamicElement);
|
|
|
| /** Returns true if t is a subtype of s */
|
| bool isSubtype(Type t, Type s) {
|
| - if (t === s || t === dynamicType || s === dynamicType ||
|
| - // TODO(karlklose): Test for s.element === compiler.objectClass.
|
| - s.name == const SourceString('Object')) return true;
|
| + if (t === s ||
|
| + t === dynamicType ||
|
| + s === dynamicType ||
|
| + s.element === compiler.objectClass) {
|
| + return true;
|
| + }
|
| + t = t.unalias(compiler);
|
| + s = s.unalias(compiler);
|
| +
|
| if (t is VoidType) {
|
| return false;
|
| } else if (t is InterfaceType) {
|
| @@ -146,6 +461,7 @@ class Types {
|
| }
|
| return false;
|
| } else if (t is FunctionType) {
|
| + if (s.element == compiler.functionClass) return true;
|
| if (s is !FunctionType) return false;
|
| FunctionType tf = t;
|
| FunctionType sf = s;
|
| @@ -206,9 +522,12 @@ class TypeCheckerVisitor implements Visitor<Type> {
|
| listType = compiler.listClass.computeType(compiler);
|
| }
|
|
|
| - Type fail(node, [reason]) {
|
| + Type fail(Node node, [reason]) {
|
| String message = 'cannot type-check';
|
| - if (reason !== null) {
|
| + if (node != null) {
|
| + message = '$message ${node.getObjectDescription()} `$node`';
|
| + }
|
| + if (reason != null) {
|
| message = '$message: $reason';
|
| }
|
| throw new CancelTypeCheckException(node, message);
|
| @@ -394,6 +713,37 @@ class TypeCheckerVisitor implements Visitor<Type> {
|
| return types.dynamicType;
|
| }
|
|
|
| + /**
|
| + * Returns the interface type on which to lookup members. If [type] is the
|
| + * function of a getter, the return type is returned. If [type] is a type
|
| + * variable the bound is returned.
|
| + */
|
| + InterfaceType findReceiverType(Node receiver, Type receiverType) {
|
| + if (receiverType === null) {
|
| + fail(receiver, 'receivertype is null');
|
| + }
|
| + if (receiverType.element == compiler.dynamicClass) {
|
| + return receiverType;
|
| + }
|
| + ElementKind receiverKind = receiverType.element.kind;
|
| + if (receiverKind === ElementKind.GETTER) {
|
| + FunctionType getterType = receiverType;
|
| + return findReceiverType(receiver, getterType.returnType);
|
| + }
|
| + if (receiverKind === ElementKind.TYPEDEF) {
|
| + // TODO(karlklose): handle typedefs.
|
| + return null;
|
| + }
|
| + if (receiverKind === ElementKind.TYPE_VARIABLE) {
|
| + TypeVariableElement typeVariableElement = receiverType.element;
|
| + return findReceiverType(receiver, typeVariableElement.bound);
|
| + }
|
| + if (receiverKind !== ElementKind.CLASS) {
|
| + fail(receiver, 'unexpected receiver kind: ${receiverKind}');
|
| + }
|
| + return receiverType;
|
| + }
|
| +
|
| void analyzeArguments(Send send, FunctionType funType) {
|
| Link<Node> arguments = send.arguments;
|
| if (funType === null || funType === types.dynamicType) {
|
| @@ -459,56 +809,87 @@ class TypeCheckerVisitor implements Visitor<Type> {
|
|
|
| } else if (node.isPropertyAccess) {
|
| if (node.receiver !== null) {
|
| - // TODO(karlklose): we cannot handle fields.
|
| - return unhandledExpression();
|
| - }
|
| - Element element = elements[node];
|
| - if (element === null) return types.dynamicType;
|
| - return computeType(element);
|
| + InterfaceType receiverType =
|
| + findReceiverType(node.receiver, analyze(node.receiver));
|
| + if (receiverType === null) return null;
|
| + if (receiverType.element === compiler.dynamicClass) {
|
| + return types.dynamicType;
|
| + }
|
|
|
| + Member member = receiverType.lookupMember(compiler, selector.source);
|
| + if (member === null ||
|
| + !(member.element.kind === ElementKind.FIELD ||
|
| + member.element.kind === ElementKind.ABSTRACT_FIELD)) {
|
| + reportTypeWarning(selector, MessageKind.PROPERTY_NOT_FOUND,
|
| + [receiverType, selector.source]);
|
| + } else {
|
| + return member.computeType(compiler);
|
| + }
|
| + return types.dynamicType;
|
| + } else {
|
| + Element element = elements[node];
|
| + if (element === null) {
|
| + return types.dynamicType;
|
| + }
|
| + if ((element.kind == ElementKind.FIELD ||
|
| + element.kind == ElementKind.GETTER) &&
|
| + element.isInstanceMember()) {
|
| + // Ensure substitution of declared type variables with inherited type
|
| + // variables.
|
| + InterfaceType thisType = currentClass.computeType(compiler);
|
| + Member member = thisType.lookupMember(compiler, selector.source);
|
| + if (member != null) {
|
| + // Should always work!
|
| + return member.computeType(compiler);
|
| + }
|
| + compiler.internalError('Could not lookup resolved member '
|
| + '${selector.source} on ${thisType}.');
|
| + }
|
| + return computeType(element);
|
| + }
|
| } else if (node.isFunctionObjectInvocation) {
|
| fail(node.receiver, 'function object invocation unimplemented');
|
|
|
| } else {
|
| FunctionType computeFunType() {
|
| if (node.receiver !== null) {
|
| - Type receiverType = analyze(node.receiver);
|
| - if (receiverType.element == compiler.dynamicClass) return null;
|
| - if (receiverType === null) {
|
| - fail(node.receiver, 'receivertype is null');
|
| - }
|
| - if (receiverType.element.kind === ElementKind.GETTER) {
|
| - FunctionType getterType = receiverType;
|
| - receiverType = getterType.returnType;
|
| - }
|
| - ElementKind receiverKind = receiverType.element.kind;
|
| - if (receiverKind === ElementKind.TYPEDEF) {
|
| - // TODO(karlklose): handle typedefs.
|
| + InterfaceType receiverType =
|
| + findReceiverType(node.receiver, analyze(node.receiver));
|
| + if (receiverType === null) return null;
|
| + if (receiverType.element === compiler.dynamicClass) return null;
|
| +
|
| + Member member = receiverType.lookupMember(compiler, selector.source);
|
| + if (member === null || member.element.kind !== ElementKind.FUNCTION) {
|
| + reportTypeWarning(selector, MessageKind.METHOD_NOT_FOUND,
|
| + [receiverType, name]);
|
| return null;
|
| }
|
| - if (receiverKind === ElementKind.TYPE_VARIABLE) {
|
| - // TODO(karlklose): handle type variables.
|
| - return null;
|
| - }
|
| - if (receiverKind !== ElementKind.CLASS) {
|
| - fail(node.receiver, 'unexpected receiver kind: ${receiverKind}');
|
| - }
|
| - ClassElement classElement = receiverType.element;
|
| - // TODO(karlklose): substitute type arguments.
|
| - Type memberType =
|
| - lookupMethodType(selector, classElement, selector.source);
|
| - if (memberType.element === compiler.dynamicClass) return null;
|
| + Type memberType = member.computeType(compiler);
|
| + if (memberType.element == compiler.dynamicClass) return null;
|
| + if (memberType.element == compiler.functionClass) return null;
|
| return memberType;
|
| } else {
|
| Element element = elements[node];
|
| if (element === null) {
|
| fail(node, 'unresolved ${node.selector}');
|
| } else if (element.kind === ElementKind.FUNCTION) {
|
| + if (element.isInstanceMember()) {
|
| + // Ensure substitution of declared type variables with inherited
|
| + // type variables.
|
| + InterfaceType thisType = currentClass.computeType(compiler);
|
| + Member member = thisType.lookupMember(compiler, selector.source);
|
| + if (member != null) {
|
| + // Should always work!
|
| + return member.computeType(compiler);
|
| + }
|
| + compiler.internalError('Could not lookup resolved member '
|
| + '${selector.source} on ${thisType}.');
|
| + }
|
| return computeType(element);
|
| } else if (element.kind === ElementKind.FOREIGN) {
|
| return null;
|
| - } else if (element.kind === ElementKind.VARIABLE
|
| - || element.kind === ElementKind.FIELD) {
|
| + } else if (element.kind === ElementKind.VARIABLE ||
|
| + element.kind === ElementKind.FIELD) {
|
| // TODO(karlklose): handle object invocations.
|
| return null;
|
| } else {
|
| @@ -565,13 +946,22 @@ class TypeCheckerVisitor implements Visitor<Type> {
|
| }
|
|
|
| Type visitNewExpression(NewExpression node) {
|
| + InterfaceType type = elements.getType(node.getTypeAnnotation());
|
| Element element = elements[node.send];
|
| - analyzeArguments(node.send, computeType(element));
|
| - return analyze(node.send.selector);
|
| + var member = new Member(type,
|
| + element.enclosingElement.computeType(compiler),
|
| + element);
|
| + analyzeArguments(node.send, member.computeType(compiler));
|
| + return type;
|
| }
|
|
|
| Type visitLiteralList(LiteralList node) {
|
| - return listType;
|
| + Type elementType = node.type != null
|
| + ? elements[node.type]
|
| + : compiler.types.dynamicType;
|
| + var linkBuilder = new LinkBuilder<Type>();
|
| + linkBuilder.addLast(elementType);
|
| + return new InterfaceType(listType.element, linkBuilder.toLink());
|
| }
|
|
|
| Type visitNodeList(NodeList node) {
|
|
|