diff graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java @ 16068:74e142bd2b12

Truffle/Source: major API revision - All source-related classes now in com.oracle.truffle.api.source - SourceFactory replaced with factory methods on Source - Revision, renaming, and documentation to methods on Source and SourceSection - NullSourceSection is now a utility class
author Michael Van De Vanter <michael.van.de.vanter@oracle.com>
date Fri, 06 Jun 2014 22:13:00 -0700
parents
children 6f7d3f3703d3
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/graal/com.oracle.truffle.api/src/com/oracle/truffle/api/source/Source.java	Fri Jun 06 22:13:00 2014 -0700
@@ -0,0 +1,823 @@
+/*
+ * Copyright (c) 2013, 2014, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+package com.oracle.truffle.api.source;
+
+import java.io.*;
+import java.lang.ref.*;
+import java.net.*;
+import java.util.*;
+
+/**
+ * Representation of a guest language source code unit and its contents. Sources originate in
+ * several ways:
+ * <ul>
+ * <li><strong>Literal:</strong> A named text string. These are not indexed and should be considered
+ * value objects; equality is defined based on contents. <br>
+ * See {@link Source#fromText(String, String)}</li>
+ * <p>
+ * <li><strong>File:</strong> Each file is represented as a canonical object, indexed by the
+ * absolute, canonical path name of the file. File contents are <em>read lazily</em> and contents
+ * optionally <em>cached</em>. <br>
+ * See {@link Source#fromFileName(String)}<br>
+ * See {@link Source#fromFileName(String, boolean)}</li>
+ * <p>
+ * <li><strong>URL:</strong> Each URL source is represented as a canonical object, indexed by the
+ * URL. Contents are <em>read eagerly</em> and <em>cached</em>. <br>
+ * See {@link Source#fromURL(URL, String)}</li>
+ * <p>
+ * <li><strong>Reader:</strong> Contents are <em>read eagerly</em> and treated as a <em>Literal</em>
+ * . <br>
+ * See {@link Source#fromReader(Reader, String)}</li>
+ * <p>
+ * <li><strong>Pseudo File:</strong> A literal text string that can be retrieved by name as if it
+ * were a file, unlike literal sources; useful for testing. <br>
+ * See {@link Source#asPseudoFile(String, String)}</li>
+ * </ul>
+ * <p>
+ * <strong>File cache:</strong>
+ * <ol>
+ * <li>File content caching is optional, <em>off</em> by default.</li>
+ * <li>The first access to source file contents will result in the contents being read, and (if
+ * enabled) cached.</li>
+ * <li>If file contents have been cached, access to contents via {@link Source#getInputStream()} or
+ * {@link Source#getReader()} will be provided from the cache.</li>
+ * <li>Any access to file contents via the cache will result in a timestamp check and possible cache
+ * reload.</li>
+ * </ol>
+ */
+public abstract class Source {
+
+    // TODO (mlvdv) consider canonicalizing and reusing SourceSection instances
+    // TOOD (mlvdv) connect SourceSections into a spatial tree for fast geometric lookup
+
+    // Files and pseudo files are indexed.
+    private static final Map<String, WeakReference<Source>> filePathToSource = new HashMap<>();
+
+    private static boolean fileCacheEnabled = true;
+
+    /**
+     * Gets the canonical representation of a source file, whose contents will be read lazily and
+     * then cached.
+     *
+     * @param fileName name
+     * @param reset forces any existing {@link Source} cache to be cleared, forcing a re-read
+     * @return canonical representation of the file's contents.
+     * @throws IOException if the file can not be read
+     */
+    public static Source fromFileName(String fileName, boolean reset) throws IOException {
+
+        final WeakReference<Source> nameRef = filePathToSource.get(fileName);
+        Source source = nameRef == null ? null : nameRef.get();
+        if (source == null) {
+            final File file = new File(fileName);
+            if (!file.canRead()) {
+                throw new IOException("Can't read file " + fileName);
+            }
+            final String path = file.getCanonicalPath();
+            final WeakReference<Source> pathRef = filePathToSource.get(path);
+            source = pathRef == null ? null : pathRef.get();
+            if (source == null) {
+                source = new FileSource(file, fileName, path);
+                filePathToSource.put(path, new WeakReference<>(source));
+            }
+        }
+        if (reset) {
+            source.reset();
+        }
+        return source;
+    }
+
+    /**
+     * Gets the canonical representation of a source file, whose contents will be read lazily and
+     * then cached.
+     *
+     * @param fileName name
+     * @return canonical representation of the file's contents.
+     * @throws IOException if the file can not be read
+     */
+    public static Source fromFileName(String fileName) throws IOException {
+        return fromFileName(fileName, false);
+    }
+
+    /**
+     * Creates a non-canonical source from literal text.
+     *
+     * @param code textual source code
+     * @param description a note about the origin, for error messages and debugging
+     * @return a newly created, non-indexed source representation
+     */
+    public static Source fromText(String code, String description) {
+        assert code != null;
+        return new LiteralSource(description, code);
+    }
+
+    /**
+     * Creates a source whose contents will be read immediately from a URL and cached.
+     *
+     * @param url
+     * @param name identifies the origin, possibly useful for debugging
+     * @return a newly created, non-indexed source representation
+     * @throws IOException if reading fails
+     */
+    public static Source fromURL(URL url, String name) throws IOException {
+        return URLSource.get(url, name);
+    }
+
+    /**
+     * Creates a source whose contents will be read immediately and cached.
+     *
+     * @param reader
+     * @param description a note about the origin, possibly useful for debugging
+     * @return a newly created, non-indexed source representation
+     * @throws IOException if reading fails
+     */
+    public static Source fromReader(Reader reader, String description) throws IOException {
+        return new LiteralSource(description, read(reader));
+    }
+
+    /**
+     * Creates a source from literal text, but which acts as a file and can be retrieved by name
+     * (unlike other literal sources); intended for testing.
+     *
+     * @param code textual source code
+     * @param pseudoFileName string to use for indexing/lookup
+     * @return a newly created, source representation, canonical with respect to its name
+     */
+    public static Source asPseudoFile(String code, String pseudoFileName) {
+        final Source source = new LiteralSource(pseudoFileName, code);
+        filePathToSource.put(pseudoFileName, new WeakReference<>(source));
+        return source;
+    }
+
+    /**
+     * Enables/disables caching of file contents, <em>disabled</em> by default. Caching of sources
+     * created from literal text or readers is always enabled.
+     */
+    public static void setFileCaching(boolean enabled) {
+        fileCacheEnabled = enabled;
+    }
+
+    private static String read(Reader reader) throws IOException {
+        final StringBuilder builder = new StringBuilder();
+        final char[] buffer = new char[1024];
+
+        while (true) {
+            final int n = reader.read(buffer);
+            if (n == -1) {
+                break;
+            }
+            builder.append(buffer, 0, n);
+        }
+
+        return builder.toString();
+    }
+
+    protected Source() {
+    }
+
+    protected TextMap textMap = null;
+
+    protected abstract void reset();
+
+    /**
+     * Returns the name of this resource holding a guest language program. An example would be the
+     * name of a guest language source code file.
+     *
+     * @return the name of the guest language program
+     */
+    public abstract String getName();
+
+    /**
+     * Returns a short version of the name of the resource holding a guest language program (as
+     * described in @getName). For example, this could be just the name of the file, rather than a
+     * full path.
+     *
+     * @return the short name of the guest language program
+     */
+    public abstract String getShortName();
+
+    /**
+     * The normalized, canonical name if the source is a file.
+     */
+    public abstract String getPath();
+
+    /**
+     * The URL if the source is retrieved via URL.
+     */
+    public abstract URL getURL();
+
+    /**
+     * Access to the source contents.
+     */
+    public abstract Reader getReader();
+
+    /**
+     * Access to the source contents.
+     */
+    public final InputStream getInputStream() {
+        return new ByteArrayInputStream(getCode().getBytes());
+    }
+
+    /**
+     * Return the complete text of the code.
+     */
+    public abstract String getCode();
+
+    /**
+     * Gets the text (not including a possible terminating newline) in a (1-based) numbered line.
+     */
+    public final String getCode(int lineNumber) {
+        checkTextMap();
+        final int offset = textMap.lineStartOffset(lineNumber);
+        final int length = textMap.lineLength(lineNumber);
+        return getCode().substring(offset, offset + length);
+    }
+
+    /**
+     * The number of text lines in the source, including empty lines; characters at the end of the
+     * source without a terminating newline count as a line.
+     */
+    public final int getLineCount() {
+        return checkTextMap().lineCount();
+    }
+
+    /**
+     * Given a 0-based character offset, return the 1-based number of the line that includes the
+     * position.
+     */
+    public final int getLineNumber(int offset) {
+        return checkTextMap().offsetToLine(offset);
+    }
+
+    /**
+     * Given a 1-based line number, return the 0-based offset of the first character in the line.
+     */
+    public final int getLineStartOffset(int lineNumber) {
+        return checkTextMap().lineStartOffset(lineNumber);
+    }
+
+    /**
+     * The number of characters (not counting a possible terminating newline) in a (1-based)
+     * numbered line.
+     */
+    public final int getLineLength(int lineNumber) {
+        return checkTextMap().lineLength(lineNumber);
+    }
+
+    /**
+     * Creates a representation of a contiguous region of text in the source.
+     * <p>
+     * This method performs no checks on the validity of the arguments.
+     * <p>
+     * The resulting representation defines hash/equality around equivalent location, presuming that
+     * {@link Source} representations are canonical.
+     *
+     * @param identifier terse description of the region
+     * @param startLine 1-based line number of the first character in the section
+     * @param startColumn 1-based column number of the first character in the section
+     * @param charIndex the 0-based index of the first character of the section
+     * @param length the number of characters in the section
+     * @return newly created object representing the specified region
+     */
+    public final SourceSection createSection(String identifier, int startLine, int startColumn, int charIndex, int length) {
+        return new DefaultSourceSection(this, identifier, startLine, startColumn, charIndex, length);
+    }
+
+    /**
+     * Creates a representation of a contiguous region of text in the source. Computes the
+     * {@code charIndex} value by building a {@linkplain TextMap map} of lines in the source.
+     * <p>
+     * Checks the position arguments for consistency with the source.
+     * <p>
+     * The resulting representation defines hash/equality around equivalent location, presuming that
+     * {@link Source} representations are canonical.
+     *
+     * @param identifier terse description of the region
+     * @param startLine 1-based line number of the first character in the section
+     * @param startColumn 1-based column number of the first character in the section
+     * @param length the number of characters in the section
+     * @return newly created object representing the specified region
+     * @throws IllegalArgumentException if arguments are outside the text of the source
+     * @throws IllegalStateException if the source is one of the "null" instances
+     */
+    public final SourceSection createSection(String identifier, int startLine, int startColumn, int length) {
+        checkTextMap();
+        final int lineStartOffset = textMap.lineStartOffset(startLine);
+        if (startColumn > textMap.lineLength(startLine)) {
+            throw new IllegalArgumentException("column out of range");
+        }
+        final int startOffset = lineStartOffset + startColumn - 1;
+        return new DefaultSourceSection(this, identifier, startLine, startColumn, startOffset, length);
+    }
+
+    /**
+     * Creates a representation of a contiguous region of text in the source. Computes the
+     * {@code (startLine, startColumn)} values by building a {@linkplain TextMap map} of lines in
+     * the source.
+     * <p>
+     * Checks the position arguments for consistency with the source.
+     * <p>
+     * The resulting representation defines hash/equality around equivalent location, presuming that
+     * {@link Source} representations are canonical.
+     *
+     *
+     * @param identifier terse description of the region
+     * @param charIndex 0-based position of the first character in the section
+     * @param length the number of characters in the section
+     * @return newly created object representing the specified region
+     * @throws IllegalArgumentException if either of the arguments are outside the text of the
+     *             source
+     * @throws IllegalStateException if the source is one of the "null" instances
+     */
+    public final SourceSection createSection(String identifier, int charIndex, int length) throws IllegalArgumentException {
+        final int codeLength = getCode().length();
+        if (!(charIndex >= 0 && length >= 0 && charIndex + length <= codeLength)) {
+            throw new IllegalArgumentException("text positions out of range");
+        }
+        checkTextMap();
+        final int startLine = getLineNumber(charIndex);
+        final int startColumn = charIndex - getLineStartOffset(startLine) + 1;
+
+        return new DefaultSourceSection(this, identifier, startLine, startColumn, charIndex, length);
+    }
+
+    /**
+     * Creates a representation of a line of text in the source identified only by line number, from
+     * which the character information will be computed.
+     *
+     * @param identifier terse description of the line
+     * @param lineNumber 1-based line number of the first character in the section
+     * @return newly created object representing the specified line
+     * @throws IllegalArgumentException if the line does not exist the source
+     * @throws IllegalStateException if the source is one of the "null" instances
+     */
+    public final SourceSection createSection(String identifier, int lineNumber) {
+        checkTextMap();
+        final int charIndex = textMap.lineStartOffset(lineNumber);
+        final int length = textMap.lineLength(lineNumber);
+        return createSection(identifier, charIndex, length);
+    }
+
+    /**
+     * Creates a representation of a line number in this source, suitable for use as a hash table
+     * key with equality defined to mean equivalent location.
+     *
+     * @param lineNumber a 1-based line number in this source
+     * @return a representation of a line in this source
+     */
+    public final LineLocation createLineLocation(int lineNumber) {
+        return new LineLocationImpl(this, lineNumber);
+    }
+
+    private TextMap checkTextMap() {
+        if (textMap == null) {
+            final String code = getCode();
+            if (code == null) {
+                throw new RuntimeException("can't read file " + getName());
+            }
+            textMap = new TextMap(code);
+        }
+        return textMap;
+    }
+
+    private static final class LiteralSource extends Source {
+
+        private final String name; // Name used originally to describe the source
+        private final String code;
+
+        public LiteralSource(String name, String code) {
+            this.name = name;
+            this.code = code;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getShortName() {
+            return name;
+        }
+
+        @Override
+        public String getCode() {
+            return code;
+        }
+
+        @Override
+        public String getPath() {
+            return name;
+        }
+
+        @Override
+        public URL getURL() {
+            return null;
+        }
+
+        @Override
+        public Reader getReader() {
+            return new StringReader(code);
+        }
+
+        @Override
+        protected void reset() {
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + name.hashCode();
+            result = prime * result + (code == null ? 0 : code.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (!(obj instanceof LiteralSource)) {
+                return false;
+            }
+            LiteralSource other = (LiteralSource) obj;
+            return name.equals(other.name) && code.equals(other.code);
+        }
+
+    }
+
+    private static final class FileSource extends Source {
+
+        private final File file;
+        private final String name; // Name used originally to describe the source
+        private final String path;  // Normalized path description of an actual file
+
+        private String code = null;  // A cache of the file's contents
+        private long timeStamp;      // timestamp of the cache in the file system
+
+        public FileSource(File file, String name, String path) {
+            this.file = file;
+            this.name = name;
+            this.path = path;
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getShortName() {
+            return file.getName();
+        }
+
+        @Override
+        public String getCode() {
+            if (fileCacheEnabled) {
+                if (code == null || timeStamp != file.lastModified()) {
+                    try {
+                        code = read(getReader());
+                        timeStamp = file.lastModified();
+                    } catch (IOException e) {
+                    }
+                }
+                return code;
+            }
+            try {
+                return read(new FileReader(file));
+            } catch (IOException e) {
+            }
+            return null;
+        }
+
+        @Override
+        public String getPath() {
+            return path;
+        }
+
+        @Override
+        public URL getURL() {
+            return null;
+        }
+
+        @Override
+        public Reader getReader() {
+            if (code != null && timeStamp == file.lastModified()) {
+                return new StringReader(code);
+            }
+            try {
+                return new FileReader(file);
+            } catch (FileNotFoundException e) {
+                throw new RuntimeException("Can't find file " + path);
+            }
+        }
+
+        @Override
+        protected void reset() {
+            this.code = null;
+        }
+
+    }
+
+    private static final class URLSource extends Source {
+
+        private static final Map<URL, WeakReference<URLSource>> urlToSource = new HashMap<>();
+
+        public static URLSource get(URL url, String name) throws IOException {
+            WeakReference<URLSource> sourceRef = urlToSource.get(url);
+            URLSource source = sourceRef == null ? null : sourceRef.get();
+            if (source == null) {
+                source = new URLSource(url, name);
+                urlToSource.put(url, new WeakReference<>(source));
+            }
+            return source;
+        }
+
+        private final URL url;
+        private final String name;
+        private String code = null;  // A cache of the source contents
+
+        public URLSource(URL url, String name) throws IOException {
+            this.url = url;
+            this.name = name;
+            code = read(new InputStreamReader(url.openStream()));
+        }
+
+        @Override
+        public String getName() {
+            return name;
+        }
+
+        @Override
+        public String getShortName() {
+            return name;
+        }
+
+        @Override
+        public String getPath() {
+            return url.getPath();
+        }
+
+        @Override
+        public URL getURL() {
+            return url;
+        }
+
+        @Override
+        public Reader getReader() {
+            return new StringReader(code);
+        }
+
+        @Override
+        public String getCode() {
+            return code;
+        }
+
+        @Override
+        protected void reset() {
+        }
+
+    }
+
+    private static final class DefaultSourceSection implements SourceSection {
+
+        private final Source source;
+        private final String identifier;
+        private final int startLine;
+        private final int startColumn;
+        private final int charIndex;
+        private final int charLength;
+
+        /**
+         * Creates a new object representing a contiguous text section within the source code of a
+         * guest language program's text.
+         * <p>
+         * The starting location of the section is specified using two different coordinate:
+         * <ul>
+         * <li><b>(row, column)</b>: rows and columns are 1-based, so the first character in a
+         * source file is at position {@code (1,1)}.</li>
+         * <li><b>character index</b>: 0-based offset of the character from the beginning of the
+         * source, so the first character in a file is at index {@code 0}.</li>
+         * </ul>
+         * The <b>newline</b> that terminates each line counts as a single character for the purpose
+         * of a character index. The (row,column) coordinates of a newline character should never
+         * appear in a text section.
+         * <p>
+         *
+         * @param source object representing the complete source program that contains this section
+         * @param identifier an identifier used when printing the section
+         * @param startLine the 1-based number of the start line of the section
+         * @param startColumn the 1-based number of the start column of the section
+         * @param charIndex the 0-based index of the first character of the section
+         * @param charLength the length of the section in number of characters
+         */
+        public DefaultSourceSection(Source source, String identifier, int startLine, int startColumn, int charIndex, int charLength) {
+            this.source = source;
+            this.identifier = identifier;
+            this.startLine = startLine;
+            this.startColumn = startColumn;
+            this.charIndex = charIndex;
+            this.charLength = charLength;
+        }
+
+        @Override
+        public final Source getSource() {
+            return source;
+        }
+
+        @Override
+        public final int getStartLine() {
+            return startLine;
+        }
+
+        @Override
+        public final LineLocation getLineLocation() {
+            return source.createLineLocation(startLine);
+        }
+
+        @Override
+        public final int getStartColumn() {
+            return startColumn;
+        }
+
+        @Override
+        public final int getCharIndex() {
+            return charIndex;
+        }
+
+        @Override
+        public final int getCharLength() {
+            return charLength;
+        }
+
+        @Override
+        public final int getCharEndIndex() {
+            return charIndex + charLength;
+        }
+
+        @Override
+        public final String getIdentifier() {
+            return identifier;
+        }
+
+        @Override
+        public final String getCode() {
+            return getSource().getCode().substring(charIndex, charIndex + charLength);
+        }
+
+        @Override
+        public final String getShortDescription() {
+            return String.format("%s:%d", source.getShortName(), startLine);
+        }
+
+        @Override
+        public String toString() {
+            return String.format("%s:%d", source.getName(), startLine);
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + charIndex;
+            result = prime * result + charLength;
+            result = prime * result + ((identifier == null) ? 0 : identifier.hashCode());
+            result = prime * result + ((source == null) ? 0 : source.hashCode());
+            result = prime * result + startColumn;
+            result = prime * result + startLine;
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (!(obj instanceof DefaultSourceSection)) {
+                return false;
+            }
+            DefaultSourceSection other = (DefaultSourceSection) obj;
+            if (charIndex != other.charIndex) {
+                return false;
+            }
+            if (charLength != other.charLength) {
+                return false;
+            }
+            if (identifier == null) {
+                if (other.identifier != null) {
+                    return false;
+                }
+            } else if (!identifier.equals(other.identifier)) {
+                return false;
+            }
+            if (source == null) {
+                if (other.source != null) {
+                    return false;
+                }
+            } else if (!source.equals(other.source)) {
+                return false;
+            }
+            if (startColumn != other.startColumn) {
+                return false;
+            }
+            if (startLine != other.startLine) {
+                return false;
+            }
+            return true;
+        }
+
+    }
+
+    private static final class LineLocationImpl implements LineLocation {
+        private final Source source;
+        private final int line;
+
+        public LineLocationImpl(Source source, int line) {
+            assert source != null;
+            this.source = source;
+            this.line = line;
+        }
+
+        @Override
+        public Source getSource() {
+            return source;
+        }
+
+        @Override
+        public int getLineNumber() {
+            return line;
+        }
+
+        @Override
+        public String toString() {
+            return "SourceLine [" + source.getName() + ", " + line + "]";
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + line;
+            result = prime * result + source.hashCode();
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (!(obj instanceof LineLocationImpl)) {
+                return false;
+            }
+            LineLocationImpl other = (LineLocationImpl) obj;
+            if (line != other.line) {
+                return false;
+            }
+            return source.equals(other.source);
+        }
+
+        @Override
+        public int compareTo(Object o) {
+            final LineLocationImpl other = (LineLocationImpl) o;
+            final int nameOrder = source.getName().compareTo(other.source.getName());
+            if (nameOrder != 0) {
+                return nameOrder;
+            }
+            return Integer.compare(line, other.line);
+        }
+
+    }
+}