/**
* Copyright 2012 Tobias Gierke <tobias.gierke@code-sourcery.de>
*
* 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 de.codesourcery.jasm16.compiler;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.apache.commons.lang.ArrayUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import de.codesourcery.jasm16.Address;
import de.codesourcery.jasm16.ast.AST;
import de.codesourcery.jasm16.compiler.io.FileResource;
import de.codesourcery.jasm16.compiler.io.IResource;
import de.codesourcery.jasm16.compiler.io.IResource.ResourceType;
import de.codesourcery.jasm16.compiler.io.StringResource;
import de.codesourcery.jasm16.exceptions.CircularSourceIncludeException;
import de.codesourcery.jasm16.utils.ITextRegion;
import de.codesourcery.jasm16.utils.Line;
import de.codesourcery.jasm16.utils.TextRegion;
/**
* Default {@link ICompilationUnit} implementation.
*
* @author tobias.gierke@code-sourcery.de
*/
public final class CompilationUnit implements ICompilationUnit {
private static final Logger LOG = Logger.getLogger(CompilationUnit.class);
private final String identifier;
private final Map<String,List<IMarker>> markers = new HashMap<String,List<IMarker>>();
private final IResource resource;
private volatile AST ast;
private volatile Address objectCodeStartAddress = Address.ZERO;
private final List<ICompilationUnit> dependencies;
private final ISymbolTable symbolTable;
private final RelocationTable relocationTable;
private Map<Integer,Line> lines = new HashMap<Integer,Line>();
private CompilationUnit(CompilationUnit unit, IResource resource)
{
this.identifier = unit.getIdentifier();
// this.markers = unit.markers;
this.resource = resource;
// this.ast = unit.getAST();
// this.objectCodeStartAddress = unit.getObjectCodeStartOffset();
this.dependencies = unit.dependencies;
this.symbolTable = unit.symbolTable;
this.relocationTable = unit.relocationTable;
this.lines = unit.lines;
}
protected CompilationUnit(String identifier,IResource resource)
{
if (StringUtils.isBlank(identifier)) {
throw new IllegalArgumentException(
"identifier must not be NULL/blank");
}
if ( resource == null ) {
throw new IllegalArgumentException("resource must not be NULL.");
}
if ( ! resource.hasType( ResourceType.SOURCE_CODE ) ) {
throw new IllegalArgumentException("Cannot create compilation unit from resource "+resource);
}
this.dependencies = new ArrayList<ICompilationUnit>();
this.resource = resource;
this.identifier=identifier;
this.symbolTable = new SymbolTable("CompilationUnit: "+resource);
this.relocationTable = new RelocationTable();
}
@Override
public ICompilationUnit withResource(IResource resource)
{
return new CompilationUnit(this,resource);
}
@Override
public void dumpSourceLines()
{
List<Integer> keys = new ArrayList<>(lines.keySet());
Collections.sort(keys);
for ( Integer key : keys ) {
System.out.println( lines.get(key) );
}
}
@Override
public void beforeCompilationStart()
{
System.err.println("----------------- before compilation of "+this+" -----------------");
this.relocationTable.clear();
this.lines.clear();
this.ast = null;
this.symbolTable.clear();
this.symbolTable.setParent( null );
this.markers.remove( IMarker.TYPE_COMPILATION_WARNING);
this.markers.remove( IMarker.TYPE_COMPILATION_ERROR );
this.markers.remove( IMarker.TYPE_GENERIC_COMPILATION_ERROR );
this.objectCodeStartAddress=Address.ZERO;
this.dependencies.clear();
}
public static ICompilationUnit createInstance(final String identifier, final IResource resource)
{
return new CompilationUnit(identifier,resource);
}
public static ICompilationUnit createInstance(final String identifier, final String source)
{
return new CompilationUnit( identifier , new StringResource( identifier , source , ResourceType.SOURCE_CODE) );
}
@Override
public RelocationTable getRelocationTable()
{
return relocationTable;
}
@Override
public void setLine(Line l)
{
if (l == null) {
throw new IllegalArgumentException("line must not be NULL.");
}
Line existing = lines.get( l.getLineNumber() );
/*
* Do not update line references that have already been parsed.
*
* The sanity check (commented-out below) is wrong during macro expansion because
* instructions/lines in expanded macros get mapped to the
* lines of the macro definition. If the macro takes parameters,
* the absolute line starting offsets may change because of different
* parameter lengths.
*
* Example:
*
* .macro dummy(param1) // line start offset: 0
* SET A,param1 // line start offset: 21
* SET B,2 // line start offset: 37 <<<<<<<<<< sanity check would break compilation
* .endmacro
*
* dummy(X)
*
* For the marked line, parsing the source yields an absolute line start offset of 37.
* When the macro invocation is expanded (virtually at the same location of the original macro body)
* , "param1' will be replaced with 'X' , shifting the line starting offset of the marked line by -5.
* Since AST#parseInternal() unconditionally invokes ICompilationUnit#setLine() , this would trip
* our sanity check which is thus disabled.
*/
if ( existing == null )
{
lines.put( l.getLineNumber() , l );
}
// if ( existing != null && ! existing.equals( l ) ) {
// TextRegion r1= new TextRegion(existing.getLineStartingOffset() , 10 );
// TextRegion r2= new TextRegion(l.getLineStartingOffset() , 10 );
// String text1 = "<invalid region "+r1+">";
// String text2 = "<invalid region "+r2+">";
// try {
// text1 = getSource(r1);
// } catch(Exception e) { /* can't help it */ }
// try {
// text2 = getSource(r2);
// } catch(Exception e) { /* can't help it */ }
// throw new RuntimeException("Won't replace existing line "+existing+" with "+l);
// }
}
@Override
public int getParsedLineCount() {
return lines.size();
}
@Override
public SourceLocation getSourceLocation(ITextRegion textRegion)
{
final Line l = getLineForOffset( textRegion.getStartingOffset() );
return new SourceLocation(this,l,textRegion);
}
@Override
public Line getPreviousLine(Line line)
{
if (line == null) {
throw new IllegalArgumentException("line must not be NULL.");
}
Line previous = null;
for (Iterator<Line> it = lines.values().iterator(); it.hasNext();)
{
final Line l = it.next();
if ( line.equals( l ) ) {
return previous;
}
previous = l;
}
return null;
}
@Override
public Line getLineForOffset(int offset) throws NoSuchElementException
{
if ( offset < 0 ) {
throw new IllegalArgumentException("offset must not be negative");
}
Line previousLine = null;
for (Iterator<Line> it = lines.values().iterator(); it.hasNext();)
{
final Line l = it.next();
if ( l.getLineStartingOffset() == offset ) {
return l;
}
if ( previousLine != null &&
previousLine.getLineStartingOffset() < offset &&
l.getLineStartingOffset() > offset )
{
return previousLine;
}
previousLine = l;
}
if ( previousLine != null && previousLine.getLineStartingOffset() <= offset ) {
return previousLine;
}
throw new NoSuchElementException("Found no line with offset "+offset);
}
/**
*
* @param range
* @return lines ordered ascending by line number
*/
@Override
public List<Line> getLinesForRange(ITextRegion range)
{
if (range == null) {
throw new IllegalArgumentException("range must not be NULL.");
}
final List<Line> result = new ArrayList<Line>();
for ( Line l : lines.values() )
{
if ( range.contains( l.getLineStartingOffset() ) ) {
result.add( l );
}
}
Comparator<Line> comparator = new Comparator<Line>() {
@Override
public int compare(Line o1, Line o2) {
return Integer.valueOf( o1.getLineNumber() ).compareTo( Integer.valueOf( o2.getLineNumber() ) );
}
};
Collections.sort( result , comparator );
return result;
}
public static ICompilationUnit createInstance(final String identifier,final File sourceFile)
{
return new CompilationUnit(identifier , new FileResource( sourceFile , ResourceType.SOURCE_CODE ) );
}
@SuppressWarnings({ "unchecked", "cast" })
@Override
public List<ICompilationError> getErrors() {
return (List<ICompilationError>) internalGetMarkers( IMarker.TYPE_COMPILATION_ERROR , IMarker.TYPE_GENERIC_COMPILATION_ERROR );
}
@SuppressWarnings("unchecked")
@Override
public List<ICompilationError> getWarnings() {
return (List<ICompilationError>) internalGetMarkers( IMarker.TYPE_COMPILATION_WARNING );
}
@Override
public boolean hasErrors()
{
return this.markers.get( IMarker.TYPE_COMPILATION_ERROR) != null ||
this.markers.get( IMarker.TYPE_GENERIC_COMPILATION_ERROR ) != null;
}
@Override
public void addMarker(IMarker marker) {
if (marker == null) {
throw new IllegalArgumentException("marker must not be NULL");
}
LOG.debug("addMarker(): "+marker);
List<IMarker> markersByType = this.markers.get( marker.getType() );
if ( markersByType == null ) {
markersByType = new ArrayList<IMarker>();
this.markers.put( marker.getType() , markersByType );
}
markersByType.add( marker );
}
@Override
public boolean equals(Object obj) {
if ( obj == this ) {
return true;
}
if ( obj instanceof ICompilationUnit ) {
return this.identifier.equals( ((ICompilationUnit) obj).getIdentifier() );
}
return super.equals(obj);
}
@Override
public int hashCode() {
return this.identifier.hashCode();
}
@Override
public String getIdentifier() {
return identifier;
}
@Override
public AST getAST() {
return ast;
}
@Override
public void setAST(AST ast) {
if (ast == null) {
throw new IllegalArgumentException("ast must not be NULL");
}
this.ast = ast;
}
@Override
public IResource getResource()
{
return resource;
}
@Override
public String getSource(ITextRegion range) throws IOException
{
return getResource().readText(range);
}
@Override
public Line getLineByNumber(int lineNumber) throws IndexOutOfBoundsException
{
final Line result = lines.get( lineNumber );
if ( result == null ) {
throw new IndexOutOfBoundsException("No line with number "+lineNumber);
}
return result;
}
@Override
public void deleteMarker(IMarker marker)
{
final List<IMarker> markers = this.markers.get( marker.getType() );
if ( markers != null ) {
for (Iterator<IMarker> iterator = markers.iterator(); iterator.hasNext();) {
final IMarker iMarker = iterator.next();
if ( iMarker == marker ) {
iterator.remove();
break;
}
}
if ( markers.isEmpty() ) {
this.markers.remove( marker.getType() );
}
}
}
@SuppressWarnings("unchecked")
@Override
public List<IMarker> getMarkers(String... types)
{
return internalGetMarkers( types );
}
@SuppressWarnings("rawtypes")
private List internalGetMarkers(String... types)
{
final List<IMarker> result = new ArrayList<IMarker>();
if ( ArrayUtils.isEmpty( types ) )
{
for ( List<IMarker> l : this.markers.values() ) {
result.addAll( l );
}
return result;
}
final Set<String> uniqueTypes = new HashSet<String>( Arrays.asList( types ) );
for ( String expectedType : uniqueTypes )
{
final List<IMarker> existing = this.markers.get(expectedType);
if ( existing != null ) {
result.addAll( new ArrayList<IMarker>( existing ) );
}
}
return result;
}
@Override
public String toString()
{
return getResource().toString()+" (has_errors="+hasErrors()+"]";
}
@Override
public Address getObjectCodeStartOffset()
{
return objectCodeStartAddress;
}
@Override
public void setObjectCodeStartOffset(Address address)
{
if (address == null) {
throw new IllegalArgumentException("address must not be NULL.");
}
this.objectCodeStartAddress = address;
}
@Override
public void addDependency(ICompilationUnit unit)
{
if (unit == null) {
throw new IllegalArgumentException("unit must not be NULL");
}
checkForCircularDependencies( unit );
this.dependencies.add( unit );
}
private void checkForCircularDependencies(ICompilationUnit unit)
{
final Set<String> resourceIdentifiers = new HashSet<String>();
final List<ICompilationUnit> newSet = new ArrayList<ICompilationUnit>( this.dependencies );
newSet.add( unit );
for ( ICompilationUnit current : newSet ) {
checkForCircularDependencies( current , resourceIdentifiers );
}
}
private void checkForCircularDependencies(ICompilationUnit current, Set<String> resourceIdentifiers)
{
if ( resourceIdentifiers.contains( current.getResource().getIdentifier() ) ) {
LOG.error("createParseContextForInclude(): Circular dependency detected: resource '"+resource.getIdentifier()+"'");
throw new CircularSourceIncludeException( "Detected circular source inclusion" , current );
}
resourceIdentifiers.add( current.getResource().getIdentifier() );
for ( ICompilationUnit dep : current.getDependencies() ) {
checkForCircularDependencies( dep , resourceIdentifiers );
}
}
@Override
public List<ICompilationUnit> getDependencies() {
return new ArrayList<ICompilationUnit>( this.dependencies );
}
@Override
public ISymbolTable getSymbolTable()
{
return symbolTable;
}
@Override
public List<Line> getLines()
{
return new ArrayList<>( this.lines.values() );
}
}