/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */

"use strict";

loadRelativeToScript('utility.js');
loadRelativeToScript('annotations.js');
loadRelativeToScript('callgraph.js');
loadRelativeToScript('dumpCFG.js');

///////////////////////////////////////////////////////////////////////////////
// Annotations
///////////////////////////////////////////////////////////////////////////////

function checkExternalFunction(entry)
{
    var whitelist = [
        "__builtin_clz",
        "__builtin_expect",
        "isprint",
        "ceilf",
        "floorf",
        /^rusturl/,
        "memcmp",
        "strcmp",
        "fmod",
        "floor",
        "ceil",
        "atof",
        /memchr/,
        "strlen",
        "Servo_ComputedValues_EqualCustomProperties",
        /Servo_DeclarationBlock_GetCssText/,
        "Servo_GetArcStringData",
        "Servo_IsWorkerThread",
        /nsIFrame::AppendOwnedAnonBoxes/,
        // Assume that atomic accesses are threadsafe.
        /^__atomic_fetch_/,
        /^__atomic_load_/,
        /^__atomic_store_/,
        /^__atomic_thread_fence/,
    ];
    if (entry.matches(whitelist))
        return;

    // memcpy and memset are safe if the target pointer is threadsafe.
    const simpleWrites = [
        "memcpy",
        "memset",
        "memmove",
    ];

    if (entry.isSafeArgument(1) && simpleWrites.includes(entry.name))
        return;

    dumpError(entry, null, "External function");
}

function hasThreadsafeReferenceCounts(entry, regexp)
{
    // regexp should match some nsISupports-operating function and produce the
    // name of the nsISupports class via exec().

    // nsISupports classes which have threadsafe reference counting.
    var whitelist = [
        "nsIRunnable",

        // I don't know if these always have threadsafe refcounts.
        "nsAtom",
        "nsIPermissionManager",
        "nsIURI",
    ];

    var match = regexp.exec(entry.name);
    return match && nameMatchesArray(match[1], whitelist);
}

function checkOverridableVirtualCall(entry, location, callee)
{
    // We get here when a virtual call is made on a structure which might be
    // overridden by script or by a binary extension. This includes almost
    // everything under nsISupports, however, so for the most part we ignore
    // this issue. The exception is for nsISupports AddRef/Release, which are
    // not in general threadsafe and whose overrides will not be generated by
    // the callgraph analysis.
    if (callee != "nsISupports.AddRef" && callee != "nsISupports.Release")
        return;

    if (hasThreadsafeReferenceCounts(entry, /::~?nsCOMPtr\(.*?\[with T = (.*?)\]$/))
        return;
    if (hasThreadsafeReferenceCounts(entry, /RefPtrTraits.*?::Release.*?\[with U = (.*?)\]/))
        return;
    if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::assign_assuming_AddRef.*?\[with T = (.*?)\]/))
        return;
    if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::assign_with_AddRef.*?\[with T = (.*?)\]/))
        return;

    // Watch for raw addref/release.
    var whitelist = [
        "Gecko_AddRefAtom",
        "Gecko_ReleaseAtom",
        /nsPrincipal::Get/,
        /CounterStylePtr::Reset/,
    ];
    if (entry.matches(whitelist))
        return;

    dumpError(entry, location, "AddRef/Release on nsISupports");
}

function checkIndirectCall(entry, location, callee)
{
    var name = entry.name;

    // These hash table callbacks should be threadsafe.
    if (/PLDHashTable/.test(name) && (/matchEntry/.test(callee) || /hashKey/.test(callee)))
        return;
    if (/PL_HashTable/.test(name) && /keyCompare/.test(callee))
        return;

    dumpError(entry, location, "Indirect call " + callee);
}

function checkVariableAssignment(entry, location, variable)
{
    var name = entry.name;

    dumpError(entry, location, "Variable assignment " + variable);
}

// Annotations for function parameters, based on function name and parameter
// name + type.
function treatAsSafeArgument(entry, varName, csuName)
{
    var whitelist = [
        // These iterator classes should all be thread local. They are passed
        // in to some Servo bindings and are created on the heap by others, so
        // just ignore writes to them.
        [null, null, /StyleChildrenIterator/],
        [null, null, /ExplicitChildIterator/],

        // The use of BeginReading() to instantiate this class confuses the
        // analysis.
        [null, null, /nsReadingIterator/],

        // These classes are passed to some Servo bindings to fill in.
        [/^Gecko_/, null, "nsStyleImageLayers"],
        [/^Gecko_/, null, /FontFamilyList/],

        // RawGeckoBorrowedNode thread-mutable parameters.
        ["Gecko_SetNodeFlags", "aNode", null],
        ["Gecko_UnsetNodeFlags", "aNode", null],

        // Various Servo binding out parameters. This is a mess and there needs
        // to be a way to indicate which params are out parameters, either using
        // an attribute or a naming convention.
        ["Gecko_CopyAnimationNames", "aDest", null],
        ["Gecko_CopyFontFamilyFrom", "dst", null],
        ["Gecko_SetAnimationName", "aStyleAnimation", null],
        ["Gecko_SetCounterStyleToName", "aPtr", null],
        ["Gecko_SetCounterStyleToSymbols", "aPtr", null],
        ["Gecko_SetCounterStyleToString", "aPtr", null],
        ["Gecko_CopyCounterStyle", "aDst", null],
        ["Gecko_SetMozBinding", "aDisplay", null],
        [/ClassOrClassList/, /aClass/, null],
        ["Gecko_GetAtomAsUTF16", "aLength", null],
        ["Gecko_CopyMozBindingFrom", "aDest", null],
        ["Gecko_SetNullImageValue", "aImage", null],
        ["Gecko_SetGradientImageValue", "aImage", null],
        ["Gecko_SetImageOrientation", "aVisibility", null],
        ["Gecko_SetImageOrientationAsFromImage", "aVisibility", null],
        ["Gecko_CopyImageOrientationFrom", "aDst", null],
        ["Gecko_SetImageElement", "aImage", null],
        ["Gecko_SetLayerImageImageValue", "aImage", null],
        ["Gecko_CopyImageValueFrom", "aImage", null],
        ["Gecko_SetCursorArrayLength", "aStyleUI", null],
        ["Gecko_CopyCursorArrayFrom", "aDest", null],
        ["Gecko_SetCursorImageValue", "aCursor", null],
        ["Gecko_SetListStyleImageImageValue", "aList", null],
        ["Gecko_SetListStyleImageNone", "aList", null],
        ["Gecko_CopyListStyleImageFrom", "aList", null],
        ["Gecko_ClearStyleContents", "aContent", null],
        ["Gecko_CopyStyleContentsFrom", "aContent", null],
        ["Gecko_CopyStyleGridTemplateValues", "aGridTemplate", null],
        ["Gecko_ResetStyleCoord", null, null],
        ["Gecko_CopyClipPathValueFrom", "aDst", null],
        ["Gecko_DestroyClipPath", "aClip", null],
        ["Gecko_ResetFilters", "effects", null],
        ["Gecko_CopyFiltersFrom", "aDest", null],
        [/Gecko_CSSValue_Set/, "aCSSValue", null],
        ["Gecko_CSSValue_Drop", "aCSSValue", null],
        ["Gecko_CSSFontFaceRule_GetCssText", "aResult", null],
        ["Gecko_EnsureTArrayCapacity", "aArray", null],
        ["Gecko_ClearPODTArray", "aArray", null],
        ["Gecko_SetStyleGridTemplate", "aGridTemplate", null],
        ["Gecko_ResizeTArrayForStrings", "aArray", null],
        ["Gecko_ClearAndResizeStyleContents", "aContent", null],
        [/Gecko_ClearAndResizeCounter/, "aContent", null],
        [/Gecko_CopyCounter.*?From/, "aContent", null],
        [/Gecko_SetContentDataImageValue/, "aList", null],
        [/Gecko_SetContentData/, "aContent", null],
        ["Gecko_SetCounterFunction", "aContent", null],
        [/Gecko_EnsureStyle.*?ArrayLength/, "aArray", null],
        ["Gecko_GetOrCreateKeyframeAtStart", "aKeyframes", null],
        ["Gecko_GetOrCreateInitialKeyframe", "aKeyframes", null],
        ["Gecko_GetOrCreateFinalKeyframe", "aKeyframes", null],
        ["Gecko_AppendPropertyValuePair", "aProperties", null],
        ["Gecko_SetStyleCoordCalcValue", null, null],
        ["Gecko_StyleClipPath_SetURLValue", "aClip", null],
        ["Gecko_nsStyleFilter_SetURLValue", "aEffects", null],
        ["Gecko_nsStyleSVGPaint_CopyFrom", "aDest", null],
        ["Gecko_nsStyleSVGPaint_SetURLValue", "aPaint", null],
        ["Gecko_nsStyleSVGPaint_Reset", "aPaint", null],
        ["Gecko_nsStyleSVG_SetDashArrayLength", "aSvg", null],
        ["Gecko_nsStyleSVG_CopyDashArray", "aDst", null],
        ["Gecko_nsStyleFont_SetLang", "aFont", null],
        ["Gecko_nsStyleFont_CopyLangFrom", "aFont", null],
        ["Gecko_ClearWillChange", "aDisplay", null],
        ["Gecko_AppendWillChange", "aDisplay", null],
        ["Gecko_CopyWillChangeFrom", "aDest", null],
        ["Gecko_InitializeImageCropRect", "aImage", null],
        ["Gecko_CopyShapeSourceFrom", "aDst", null],
        ["Gecko_DestroyShapeSource", "aShape", null],
        ["Gecko_StyleShapeSource_SetURLValue", "aShape", null],
        ["Gecko_NewBasicShape", "aShape", null],
        ["Gecko_NewShapeImage", "aShape", null],
        ["Gecko_nsFont_InitSystem", "aDest", null],
        ["Gecko_nsFont_SetFontFeatureValuesLookup", "aFont", null],
        ["Gecko_nsFont_ResetFontFeatureValuesLookup", "aFont", null],
        ["Gecko_nsStyleFont_FixupNoneGeneric", "aFont", null],
        ["Gecko_StyleTransition_SetUnsupportedProperty", "aTransition", null],
        ["Gecko_AddPropertyToSet", "aPropertySet", null],
        ["Gecko_CalcStyleDifference", "aAnyStyleChanged", null],
        ["Gecko_CalcStyleDifference", "aOnlyResetStructsChanged", null],
        ["Gecko_nsStyleSVG_CopyContextProperties", "aDst", null],
        ["Gecko_nsStyleFont_PrefillDefaultForGeneric", "aFont", null],
        ["Gecko_nsStyleSVG_SetContextPropertiesLength", "aSvg", null],
        ["Gecko_ClearAlternateValues", "aFont", null],
        ["Gecko_AppendAlternateValues", "aFont", null],
        ["Gecko_CopyAlternateValuesFrom", "aDest", null],
        ["Gecko_CounterStyle_GetName", "aResult", null],
        ["Gecko_CounterStyle_GetSingleString", "aResult", null],
        ["Gecko_nsTArray_FontFamilyName_AppendNamed", "aNames", null],
        ["Gecko_nsTArray_FontFamilyName_AppendGeneric", "aNames", null],
    ];
    for (var [entryMatch, varMatch, csuMatch] of whitelist) {
        assert(entryMatch || varMatch || csuMatch);
        if (entryMatch && !nameMatches(entry.name, entryMatch))
            continue;
        if (varMatch && !nameMatches(varName, varMatch))
            continue;
        if (csuMatch && (!csuName || !nameMatches(csuName, csuMatch)))
            continue;
        return true;
    }
    return false;
}

function isSafeAssignment(entry, edge, variable)
{
    if (edge.Kind != 'Assign')
        return false;

    var [mangled, unmangled] = splitFunction(entry.name);

    // The assignment
    //
    //   nsFont* font = fontTypes[eType];
    //
    // ends up with 'font' pointing to a member of 'this', so it should inherit
    // the safety of 'this'.
    if (unmangled.includes("mozilla::LangGroupFontPrefs::Initialize") &&
        variable == 'font')
    {
        const [lhs, rhs] = edge.Exp;
        const {Kind, Exp: [{Kind: indexKind, Exp: [collection, index]}]} = rhs;
        if (Kind == 'Drf' &&
            indexKind == 'Index' &&
            collection.Kind == 'Var' &&
            collection.Variable.Name[0] == 'fontTypes')
        {
            return entry.isSafeArgument(0); // 'this'
        }
    }

    return false;
}

function checkFieldWrite(entry, location, fields)
{
    var name = entry.name;
    for (var field of fields) {
        // The analysis is having some trouble keeping track of whether
        // already_AddRefed and nsCOMPtr structures are safe to access.
        // Hopefully these will be thread local, but it would be better to
        // improve the analysis to handle these.
        if (/already_AddRefed.*?.mRawPtr/.test(field))
            return;
        if (/nsCOMPtr<.*?>.mRawPtr/.test(field))
            return;

        if (/\bThreadLocal<\b/.test(field))
            return;
    }

    var str = "";
    for (var field of fields)
        str += " " + field;

    dumpError(entry, location, "Field write" + str);
}

function checkDereferenceWrite(entry, location, variable)
{
    var name = entry.name;

    // Maybe<T> uses placement new on local storage in a way we don't understand.
    // Allow this if the Maybe<> value itself is threadsafe.
    if (/Maybe.*?::emplace/.test(name) && entry.isSafeArgument(0))
        return;

    // UniquePtr writes through temporaries referring to its internal storage.
    // Allow this if the UniquePtr<> is threadsafe.
    if (/UniquePtr.*?::reset/.test(name) && entry.isSafeArgument(0))
        return;

    // Operations on nsISupports reference counts.
    if (hasThreadsafeReferenceCounts(entry, /nsCOMPtr<T>::swap\(.*?\[with T = (.*?)\]/))
        return;

    // ConvertToLowerCase::write writes through a local pointer into the first
    // argument.
    if (/ConvertToLowerCase::write/.test(name) && entry.isSafeArgument(0))
        return;

    dumpError(entry, location, "Dereference write " + (variable ? variable : "<unknown>"));
}

function ignoreCallEdge(entry, callee)
{
    var name = entry.name;

    // nsPropertyTable::GetPropertyInternal has the option of removing data
    // from the table, but when it is called by nsPropertyTable::GetProperty
    // this will not occur.
    if (/nsPropertyTable::GetPropertyInternal/.test(callee) &&
        /nsPropertyTable::GetProperty/.test(name))
    {
        return true;
    }

    // nsIDocument::PropertyTable calls GetExtraPropertyTable (which has side
    // effects) if the input category is non-zero. If a literal zero was passed
    // in for the category then we treat it as a safe argument, per
    // isEdgeSafeArgument, so just watch for that.
    if (/nsIDocument::GetExtraPropertyTable/.test(callee) &&
        /nsIDocument::PropertyTable/.test(name) &&
        entry.isSafeArgument(1))
    {
        return true;
    }

    // This function has an explicit test for being on the main thread if the
    // style has non-threadsafe refcounts, but the analysis isn't smart enough
    // to understand what the actual styles that can be involved are.
    if (/nsStyleList::SetCounterStyle/.test(callee))
        return true;

    // CachedBorderImageData is exclusively owned by nsStyleImage, but the
    // analysis is not smart enough to know this.
    if (/CachedBorderImageData::PurgeCachedImages/.test(callee) &&
        /nsStyleImage::/.test(name) &&
        entry.isSafeArgument(0))
    {
        return true;
    }

    // StyleShapeSource exclusively owns its UniquePtr<nsStyleImage>.
    if (/nsStyleImage::SetURLValue/.test(callee) &&
        /StyleShapeSource::SetURL/.test(name) &&
        entry.isSafeArgument(0))
    {
        return true;
    }

    // The AddRef through a just-assigned heap pointer here is not handled by
    // the analysis.
    if (/nsCSSValue::Array::AddRef/.test(callee) &&
        /nsStyleContentData::SetCounters/.test(name) &&
        entry.isSafeArgument(2))
    {
        return true;
    }

    // AllChildrenIterator asks AppendOwnedAnonBoxes to append into an nsTArray
    // local variable.
    if (/nsIFrame::AppendOwnedAnonBoxes/.test(callee) &&
        /AllChildrenIterator::AppendNativeAnonymousChildren/.test(name))
    {
        return true;
    }

    // Runnables are created and named on one thread, then dispatched
    // (possibly to another). Writes on the origin thread are ok.
    if (/::SetName/.test(callee) &&
        /::UnlabeledDispatch/.test(name))
    {
        return true;
    }

    // We manually lock here
    if (name == "Gecko_nsFont_InitSystem" ||
        name == "Gecko_GetFontMetrics" ||
        name == "Gecko_nsStyleFont_FixupMinFontSize" ||
        /ThreadSafeGetDefaultFontHelper/.test(name))
    {
        return true;
    }

    return false;
}

function ignoreContents(entry)
{
    var whitelist = [
        // We don't care what happens when we're about to crash.
        "abort",
        /MOZ_ReportAssertionFailure/,
        /MOZ_ReportCrash/,
        /MOZ_CrashPrintf/,
        /MOZ_CrashOOL/,
        /AnnotateMozCrashReason/,
        /InvalidArrayIndex_CRASH/,
        /NS_ABORT_OOM/,

        // These ought to be threadsafe.
        "NS_DebugBreak",
        /mozalloc_handle_oom/,
        /^NS_Log/, /log_print/, /LazyLogModule::operator/,
        /SprintfLiteral/, "PR_smprintf", "PR_smprintf_free",
        /NS_DispatchToMainThread/, /NS_ReleaseOnMainThreadSystemGroup/,
        /NS_NewRunnableFunction/, /NS_Atomize/,
        /nsCSSValue::BufferFromString/,
        /NS_strdup/,
        /Assert_NoQueryNeeded/,
        /AssertCurrentThreadOwnsMe/,
        /PlatformThread::CurrentId/,
        /imgRequestProxy::GetProgressTracker/, // Uses an AutoLock
        /Smprintf/,
        "malloc",
        "calloc",
        "free",
        "realloc",
        "memalign",
        "strdup",
        "strndup",
        "moz_xmalloc",
        "moz_xcalloc",
        "moz_xrealloc",
        "moz_xmemalign",
        "moz_xstrdup",
        "moz_xstrndup",
        "jemalloc_thread_local_arena",

        // These all create static strings in local storage, which is threadsafe
        // to do but not understood by the analysis yet.
        / EmptyString\(\)/,
        /nsCSSProps::LookupPropertyValue/,
        /nsCSSProps::ValueToKeyword/,
        /nsCSSKeywords::GetStringValue/,

        // These could probably be handled by treating the scope of PSAutoLock
        // aka BaseAutoLock<PSMutex> as threadsafe.
        /profiler_register_thread/,
        /profiler_unregister_thread/,

        // The analysis thinks we'll write to mBits in the DoGetStyleFoo<false>
        // call.  Maybe the template parameter confuses it?
        /nsStyleContext::PeekStyle/,

        // The analysis can't cope with the indirection used for the objects
        // being initialized here, from nsCSSValue::Array::Create to the return
        // value of the Item(i) getter.
        /nsCSSValue::SetCalcValue/,

        // Unable to analyze safety of linked list initialization.
        "Gecko_NewCSSValueSharedList",
        "Gecko_CSSValue_InitSharedList",

        // Unable to trace through dataflow, but straightforward if inspected.
        "Gecko_NewNoneTransform",

        // Need main thread assertions or other fixes.
        /EffectCompositor::GetServoAnimationRule/,
    ];
    if (entry.matches(whitelist))
        return true;

    if (entry.isSafeArgument(0)) {
        var heapWhitelist = [
            // Operations on heap structures pointed to by arrays and strings are
            // threadsafe as long as the array/string itself is threadsafe.
            /nsTArray_Impl.*?::AppendElement/,
            /nsTArray_Impl.*?::RemoveElementsAt/,
            /nsTArray_Impl.*?::ReplaceElementsAt/,
            /nsTArray_Impl.*?::InsertElementAt/,
            /nsTArray_Impl.*?::SetCapacity/,
            /nsTArray_Impl.*?::SetLength/,
            /nsTArray_base.*?::EnsureCapacity/,
            /nsTArray_base.*?::ShiftData/,
            /AutoTArray.*?::Init/,
            /(nsTSubstring<T>|nsAC?String)::SetCapacity/,
            /(nsTSubstring<T>|nsAC?String)::SetLength/,
            /(nsTSubstring<T>|nsAC?String)::Assign/,
            /(nsTSubstring<T>|nsAC?String)::Append/,
            /(nsTSubstring<T>|nsAC?String)::Replace/,
            /(nsTSubstring<T>|nsAC?String)::Trim/,
            /(nsTSubstring<T>|nsAC?String)::Truncate/,
            /(nsTSubstring<T>|nsAC?String)::StripTaggedASCII/,
            /(nsTSubstring<T>|nsAC?String)::operator=/,
            /nsTAutoStringN<T, N>::nsTAutoStringN/,

            // Similar for some other data structures
            /nsCOMArray_base::SetCapacity/,
            /nsCOMArray_base::Clear/,
            /nsCOMArray_base::AppendElement/,

            // UniquePtr is similar.
            /mozilla::UniquePtr/,

            // The use of unique pointers when copying mCropRect here confuses
            // the analysis.
            /nsStyleImage::DoCopy/,
        ];
        if (entry.matches(heapWhitelist))
            return true;
    }

    if (entry.isSafeArgument(1)) {
        var firstArgWhitelist = [
            /nsTextFormatter::snprintf/,
            /nsTextFormatter::ssprintf/,
            /_ASCIIToUpperInSitu/,

            // Handle some writes into an array whose safety we don't have a good way
            // of tracking currently.
            /FillImageLayerList/,
            /FillImageLayerPositionCoordList/,
        ];
        if (entry.matches(firstArgWhitelist))
            return true;
    }

    if (entry.isSafeArgument(2)) {
        var secondArgWhitelist = [
            /nsStringBuffer::ToString/,
            /AppendUTF\d+toUTF\d+/,
            /AppendASCIItoUTF\d+/,
        ];
        if (entry.matches(secondArgWhitelist))
            return true;
    }

    return false;
}

///////////////////////////////////////////////////////////////////////////////
// Sixgill Utilities
///////////////////////////////////////////////////////////////////////////////

function variableName(variable)
{
    return (variable && variable.Name) ? variable.Name[0] : null;
}

function stripFields(exp)
{
    // Fields and index operations do not involve any dereferences. Remove them
    // from the expression but remember any encountered fields for use by
    // annotations later on.
    var fields = [];
    while (true) {
        if (exp.Kind == "Index") {
            exp = exp.Exp[0];
            continue;
        }
        if (exp.Kind == "Fld") {
            var csuName = exp.Field.FieldCSU.Type.Name;
            var fieldName = exp.Field.Name[0];
            assert(csuName && fieldName);
            fields.push(csuName + "." + fieldName);
            exp = exp.Exp[0];
            continue;
        }
        break;
    }
    return [exp, fields];
}

function isLocalVariable(variable)
{
    switch (variable.Kind) {
      case "Return":
      case "Temp":
      case "Local":
      case "Arg":
        return true;
    }
    return false;
}

function isDirectCall(edge, regexp)
{
    return edge.Kind == "Call"
        && edge.Exp[0].Kind == "Var"
        && regexp.test(variableName(edge.Exp[0].Variable));
}

function isZero(exp)
{
    return exp.Kind == "Int" && exp.String == "0";
}

///////////////////////////////////////////////////////////////////////////////
// Analysis Structures
///////////////////////////////////////////////////////////////////////////////

// Safe arguments are those which may be written through (directly, not through
// pointer fields etc.) without concerns about thread safety. This includes
// pointers to stack data, null pointers, and other data we know is thread
// local, such as certain arguments to the root functions.
//
// Entries in the worklist keep track of the pointer arguments to the function
// which are safe using a sorted array, so that this can be propagated down the
// stack. Zero is |this|, and arguments are indexed starting at one.

function WorklistEntry(name, safeArguments, stack, parameterNames)
{
    this.name = name;
    this.safeArguments = safeArguments;
    this.stack = stack;
    this.parameterNames = parameterNames;
}

WorklistEntry.prototype.readable = function()
{
    const [ mangled, readable ] = splitFunction(this.name);
    return readable;
}

WorklistEntry.prototype.mangledName = function()
{
    var str = this.name;
    for (var safe of this.safeArguments)
        str += " SAFE " + safe;
    return str;
}

WorklistEntry.prototype.isSafeArgument = function(index)
{
    for (var safe of this.safeArguments) {
        if (index == safe)
            return true;
    }
    return false;
}

WorklistEntry.prototype.setParameterName = function(index, name)
{
    this.parameterNames[index] = name;
}

WorklistEntry.prototype.addSafeArgument = function(index)
{
    if (this.isSafeArgument(index))
        return;
    this.safeArguments.push(index);

    // Sorting isn't necessary for correctness but makes printed stack info tidier.
    this.safeArguments.sort();
}

function safeArgumentIndex(variable)
{
    if (variable.Kind == "This")
        return 0;
    if (variable.Kind == "Arg")
        return variable.Index + 1;
    return -1;
}

function nameMatches(name, match)
{
    if (typeof match == "string") {
        if (name == match)
            return true;
    } else {
        assert(match instanceof RegExp);
        if (match.test(name))
            return true;
    }
    return false;
}

function nameMatchesArray(name, matchArray)
{
    for (var match of matchArray) {
        if (nameMatches(name, match))
            return true;
    }
    return false;
}

WorklistEntry.prototype.matches = function(matchArray)
{
    return nameMatchesArray(this.name, matchArray);
}

function CallSite(callee, safeArguments, location, parameterNames)
{
    this.callee = callee;
    this.safeArguments = safeArguments;
    this.location = location;
    this.parameterNames = parameterNames;
}

CallSite.prototype.safeString = function()
{
    if (this.safeArguments.length) {
        var str = "";
        for (var i = 0; i < this.safeArguments.length; i++) {
            var arg = this.safeArguments[i];
            if (arg in this.parameterNames)
                str += " " + this.parameterNames[arg];
            else
                str += " <" + ((arg == 0) ? "this" : "arg" + (arg - 1)) + ">";
        }
        return " ### SafeArguments:" + str;
    }
    return "";
}

///////////////////////////////////////////////////////////////////////////////
// Analysis Core
///////////////////////////////////////////////////////////////////////////////

var errorCount = 0;
var errorLimit = 100;

// We want to suppress output for functions that ended up not having any
// hazards, for brevity of the final output. So each new toplevel function will
// initialize this to a string, which should be printed only if an error is
// seen.
var errorHeader;

var startTime = new Date;
function elapsedTime()
{
    var seconds = (new Date - startTime) / 1000;
    return "[" + seconds.toFixed(2) + "s] ";
}

var options = parse_options([
    {
        name: '--strip-prefix',
        default: os.getenv('SOURCE') || '',
        type: 'string'
    },
    {
        name: '--add-prefix',
        default: os.getenv('URLPREFIX') || '',
        type: 'string'
    },
    {
        name: '--verbose',
        type: 'bool'
    },
]);

function add_trailing_slash(str) {
    if (str == '')
        return str;
    return str.endsWith("/") ? str : str + "/";
}

var removePrefix = add_trailing_slash(options.strip_prefix);
var addPrefix = add_trailing_slash(options.add_prefix);

if (options.verbose) {
    printErr(`Removing prefix ${removePrefix} from paths`);
    printErr(`Prepending ${addPrefix} to paths`);
}

print(elapsedTime() + "Loading types...");
if (os.getenv("TYPECACHE"))
    loadTypesWithCache('src_comp.xdb', os.getenv("TYPECACHE"));
else
    loadTypes('src_comp.xdb');
print(elapsedTime() + "Starting analysis...");

var xdb = xdbLibrary();
xdb.open("src_body.xdb");

var minStream = xdb.min_data_stream();
var maxStream = xdb.max_data_stream();
var roots = [];

var [flag, arg] = scriptArgs;
if (flag && (flag == '-f' || flag == '--function')) {
    roots = [arg];
} else {
    for (var bodyIndex = minStream; bodyIndex <= maxStream; bodyIndex++) {
        var key = xdb.read_key(bodyIndex);
        var name = key.readString();
        if (/^Gecko_/.test(name)) {
            var data = xdb.read_entry(key);
            if (/ServoBindings.cpp/.test(data.readString()))
                roots.push(name);
            xdb.free_string(data);
        }
        xdb.free_string(key);
    }
}

print(elapsedTime() + "Found " + roots.length + " roots.");
for (var i = 0; i < roots.length; i++) {
    var root = roots[i];
    errorHeader = elapsedTime() + "#" + (i + 1) + " Analyzing " + root + " ...";
    try {
        processRoot(root);
    } catch (e) {
        if (e != "Error!")
            throw e;
    }
}

print(`${elapsedTime()}Completed analysis, found ${errorCount}/${errorLimit} allowed errors`);

var currentBody;

// All local variable assignments we have seen in either the outer or inner
// function. This crosses loop boundaries, and currently has an unsoundness
// where later assignments in a loop are not taken into account.
var assignments;

// All loops in the current function which are reachable off main thread.
var reachableLoops;

// Functions that are reachable from the current root.
var reachable = {};

function dumpError(entry, location, text)
{
    if (errorHeader) {
        print(errorHeader);
        errorHeader = undefined;
    }

    var stack = entry.stack;
    print("Error: " + text);
    print("Location: " + entry.name + (location ? " @ " + location : "") + stack[0].safeString());
    print("Stack Trace:");
    // Include the callers in the stack trace instead of the callees. Make sure
    // the dummy stack entry we added for the original roots is in place.
    assert(stack[stack.length - 1].location == null);
    for (var i = 0; i < stack.length - 1; i++)
        print(stack[i + 1].callee + " @ " + stack[i].location + stack[i + 1].safeString());
    print("\n");

    if (++errorCount == errorLimit) {
        print("Maximum number of errors encountered, exiting...");
        quit();
    }

    throw "Error!";
}

// If edge is an assignment from a local variable, return the rhs variable.
function variableAssignRhs(edge)
{
    if (edge.Kind == "Assign" && edge.Exp[1].Kind == "Drf" && edge.Exp[1].Exp[0].Kind == "Var") {
        var variable = edge.Exp[1].Exp[0].Variable;
        if (isLocalVariable(variable))
            return variable;
    }
    return null;
}

function processAssign(body, entry, location, lhs, edge)
{
    var fields;
    [lhs, fields] = stripFields(lhs);

    switch (lhs.Kind) {
      case "Var":
        var name = variableName(lhs.Variable);
        if (isLocalVariable(lhs.Variable)) {
            // Remember any assignments to local variables in this function.
            // Note that we ignore any points where the variable's address is
            // taken and indirect assignments might occur. This is an
            // unsoundness in the analysis.

            let assign = [body, edge];

            // Chain assignments if the RHS has only been assigned once.
            var rhsVariable = variableAssignRhs(edge);
            if (rhsVariable) {
                var rhsAssign = singleAssignment(variableName(rhsVariable));
                if (rhsAssign)
                    assign = rhsAssign;
            }

            if (!(name in assignments))
                assignments[name] = [];
            assignments[name].push(assign);
        } else {
            checkVariableAssignment(entry, location, name);
        }
        return;
      case "Drf":
        var variable = null;
        if (lhs.Exp[0].Kind == "Var") {
            variable = lhs.Exp[0].Variable;
            if (isSafeVariable(entry, variable))
                return;
        } else if (lhs.Exp[0].Kind == "Fld") {
            const {
                Type: {Kind, Type: fieldType},
                FieldCSU: {Type: {Kind: containerTypeKind,
                                  Name: containerTypeName}}
            } = lhs.Exp[0].Field;
            const [containerExpr] = lhs.Exp[0].Exp;

            if (containerTypeKind == 'CSU' &&
                Kind == 'Pointer' &&
                isEdgeSafeArgument(entry, containerExpr) &&
                isSafeMemberPointer(containerTypeName, fieldType))
            {
                return;
            }

        }
        if (fields.length)
            checkFieldWrite(entry, location, fields);
        else
            checkDereferenceWrite(entry, location, variableName(variable));
        return;
      case "Int":
        if (isZero(lhs)) {
            // This shows up under MOZ_ASSERT, to crash the process.
            return;
        }
    }
    dumpError(entry, location, "Unknown assignment " + JSON.stringify(lhs));
}

function get_location(rawLocation) {
    const filename = rawLocation.CacheString.replace(removePrefix, '');
    return addPrefix + filename + "#" + rawLocation.Line;
}

function process(entry, body, addCallee)
{
    if (!("PEdge" in body))
        return;

    // Add any arguments which are safe due to annotations.
    if ("DefineVariable" in body) {
        for (var defvar of body.DefineVariable) {
            var index = safeArgumentIndex(defvar.Variable);
            if (index >= 0) {
                var varName = index ? variableName(defvar.Variable) : "this";
                assert(varName);
                entry.setParameterName(index, varName);
                var csuName = null;
                var type = defvar.Type;
                if (type.Kind == "Pointer" && type.Type.Kind == "CSU")
                    csuName = type.Type.Name;
                if (treatAsSafeArgument(entry, varName, csuName))
                    entry.addSafeArgument(index);
            }
        }
    }

    // Points in the body which are reachable if we are not on the main thread.
    var nonMainThreadPoints = [];
    nonMainThreadPoints[body.Index[0]] = true;

    for (var edge of body.PEdge) {
        // Ignore code that only executes on the main thread.
        if (!(edge.Index[0] in nonMainThreadPoints))
            continue;

        var location = get_location(body.PPoint[edge.Index[0] - 1].Location);

        var callees = getCallees(edge);
        for (var callee of callees) {
            switch (callee.kind) {
            case "direct":
                var safeArguments = getEdgeSafeArguments(entry, edge, callee.name);
                addCallee(new CallSite(callee.name, safeArguments, location, {}));
                break;
              case "resolved-field":
                break;
              case "field":
                var field = callee.csu + "." + callee.field;
                if (callee.isVirtual)
                    checkOverridableVirtualCall(entry, location, field);
                else
                    checkIndirectCall(entry, location, field);
                break;
              case "indirect":
                checkIndirectCall(entry, location, callee.variable);
                break;
              default:
                dumpError(entry, location, "Unknown call " + callee.kind);
                break;
            }
        }

        var fallthrough = true;

        if (edge.Kind == "Assign") {
            assert(edge.Exp.length == 2);
            processAssign(body, entry, location, edge.Exp[0], edge);
        } else if (edge.Kind == "Call") {
            assert(edge.Exp.length <= 2);
            if (edge.Exp.length == 2)
                processAssign(body, entry, location, edge.Exp[1], edge);

            // Treat assertion failures as if they don't return, so that
            // asserting NS_IsMainThread() is sufficient to prevent the
            // analysis from considering a block of code.
            if (isDirectCall(edge, /MOZ_ReportAssertionFailure/))
                fallthrough = false;
        } else if (edge.Kind == "Loop") {
            reachableLoops[edge.BlockId.Loop] = true;
        } else if (edge.Kind == "Assume") {
            if (testFailsOffMainThread(edge.Exp[0], edge.PEdgeAssumeNonZero))
                fallthrough = false;
        }

        if (fallthrough)
            nonMainThreadPoints[edge.Index[1]] = true;
    }
}

function maybeProcessMissingFunction(entry, addCallee)
{
    // If a function is missing it might be because a destructor Foo::~Foo() is
    // being called but GCC only gave us an implementation for
    // Foo::~Foo(int32). See computeCallgraph.js for a little more info.
    var name = entry.name;
    if (name.indexOf("::~") > 0 && name.indexOf("()") > 0) {
        var callee = name.replace("()", "(int32)");
        addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
        return true;
    }

    // Similarly, a call to a C1 constructor might invoke the C4 constructor. A
    // mangled constructor will be something like _ZN<length><name>C1E... or in
    // the case of a templatized constructor, _ZN<length><name>C1I...EE... so
    // we hack it and look for "C1E" or "C1I" and replace them with their C4
    // variants. This will have rare false matches, but so far we haven't hit
    // any external function calls of that sort.
    if (entry.mangledName().includes("C1E") || entry.mangledName().includes("C1I")) {
        var callee = name.replace("C1E", "C4E").replace("C1I", "C4I");
        addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
        return true;
    }

    // Hack to manually follow some typedefs that show up on some functions.
    // This is a bug in the sixgill GCC plugin I think, since sixgill is
    // supposed to follow any typedefs itself.
    if (/mozilla::dom::Element/.test(name)) {
        var callee = name.replace("mozilla::dom::Element", "nsIDocument::Element");
        addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
        return true;
    }

    // Hack for contravariant return types. When overriding a virtual method
    // with a method that returns a different return type (a subtype of the
    // original return type), we are getting the right mangled name but the
    // wrong return type in the unmangled name.
    if (/\$nsTextFrame*/.test(name)) {
        var callee = name.replace("nsTextFrame", "nsIFrame");
        addCallee(new CallSite(name, entry.safeArguments, entry.stack[0].location, entry.parameterNames));
        return true;
    }

    return false;
}

function processRoot(name)
{
    var safeArguments = [];
    var parameterNames = {};
    var worklist = [new WorklistEntry(name, safeArguments, [new CallSite(name, safeArguments, null, parameterNames)], parameterNames)];

    reachable = {};

    while (worklist.length > 0) {
        var entry = worklist.pop();

        // In principle we would be better off doing a meet-over-paths here to get
        // the common subset of arguments which are safe to write through. However,
        // analyzing functions separately for each subset if simpler, ensures that
        // the stack traces we produce accurately characterize the stack arguments,
        // and should be fast enough for now.

        if (entry.mangledName() in reachable)
            continue;
        reachable[entry.mangledName()] = true;

        if (ignoreContents(entry))
            continue;

        var data = xdb.read_entry(entry.name);
        var dataString = data.readString();
        var callees = [];
        if (dataString.length) {
            // Reverse the order of the bodies we process so that we visit the
            // outer function and see its assignments before the inner loops.
            assignments = {};
            reachableLoops = {};
            var bodies = JSON.parse(dataString).reverse();
            for (var body of bodies) {
                if (!body.BlockId.Loop || body.BlockId.Loop in reachableLoops) {
                    currentBody = body;
                    process(entry, body, Array.prototype.push.bind(callees));
                }
            }
        } else {
            if (!maybeProcessMissingFunction(entry, Array.prototype.push.bind(callees)))
                checkExternalFunction(entry);
        }
        xdb.free_string(data);

        for (var callee of callees) {
            if (!ignoreCallEdge(entry, callee.callee)) {
                var nstack = [callee, ...entry.stack];
                worklist.push(new WorklistEntry(callee.callee, callee.safeArguments, nstack, callee.parameterNames));
            }
        }
    }
}

function isEdgeSafeArgument(entry, exp)
{
    var fields;
    [exp, fields] = stripFields(exp);

    if (exp.Kind == "Var" && isLocalVariable(exp.Variable))
        return true;
    if (exp.Kind == "Drf" && exp.Exp[0].Kind == "Var") {
        var variable = exp.Exp[0].Variable;
        return isSafeVariable(entry, variable);
    }
    if (isZero(exp))
        return true;
    return false;
}

function getEdgeSafeArguments(entry, edge, callee)
{
    assert(edge.Kind == "Call");
    var res = [];
    if ("PEdgeCallInstance" in edge) {
        if (isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
            res.push(0);
    }
    if ("PEdgeCallArguments" in edge) {
        var args = edge.PEdgeCallArguments.Exp;
        for (var i = 0; i < args.length; i++) {
            if (isEdgeSafeArgument(entry, args[i]))
                res.push(i + 1);
        }
    }
    return res;
}

function singleAssignment(name)
{
    if (name in assignments) {
        var edges = assignments[name];
        if (edges.length == 1)
            return edges[0];
    }
    return null;
}

function expressionValueEdge(exp) {
    if (!(exp.Kind == "Var" && exp.Variable.Kind == "Temp"))
        return null;
    const assign = singleAssignment(variableName(exp.Variable));
    if (!assign)
        return null;
    const [body, edge] = assign;
    return edge;
}

function isSafeVariable(entry, variable)
{
    var index = safeArgumentIndex(variable);
    if (index >= 0)
        return entry.isSafeArgument(index);

    if (variable.Kind != "Temp" && variable.Kind != "Local")
        return false;
    var name = variableName(variable);

    if (!entry.safeLocals)
        entry.safeLocals = new Map;
    if (entry.safeLocals.has(name))
        return entry.safeLocals.get(name);

    const safe = isSafeLocalVariable(entry, name);
    entry.safeLocals.set(name, safe);
    return safe;
}

function isSafeLocalVariable(entry, name)
{
    // If there is a single place where this variable has been assigned on
    // edges we are considering, look at that edge.
    var assign = singleAssignment(name);
    if (assign) {
        const [body, edge] = assign;

        // Treat temporary pointers to DebugOnly contents as thread local.
        if (isDirectCall(edge, /DebugOnly.*?::operator/))
            return true;

        // Treat heap allocated pointers as thread local during construction.
        // Hopefully the construction code doesn't leak pointers to the object
        // to places where other threads might access it.
        if (isDirectCall(edge, /operator new/) ||
            isDirectCall(edge, /nsCSSValue::Array::Create/))
        {
            return true;
        }

        if ("PEdgeCallInstance" in edge) {
            // References to the contents of an array are threadsafe if the array
            // itself is threadsafe.
            if ((isDirectCall(edge, /operator\[\]/) ||
                 isDirectCall(edge, /nsTArray.*?::InsertElementAt\b/) ||
                 isDirectCall(edge, /nsStyleContent::ContentAt/)) &&
                isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
            {
                return true;
            }

            // Watch for the coerced result of a getter_AddRefs or getter_Copies call.
            if (isDirectCall(edge, /operator /)) {
                var otherEdge = expressionValueEdge(edge.PEdgeCallInstance.Exp);
                if (otherEdge &&
                    isDirectCall(otherEdge, /getter_(?:AddRefs|Copies)/) &&
                    isEdgeSafeArgument(entry, otherEdge.PEdgeCallArguments.Exp[0]))
                {
                    return true;
                }
            }

            // RefPtr::operator->() and operator* transmit the safety of the
            // RefPtr to the return value.
            if (isDirectCall(edge, /RefPtr<.*?>::operator(->|\*)\(\)/) &&
                isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
            {
                return true;
            }

            // Placement-new returns a pointer that is as safe as the pointer
            // passed to it. Exp[0] is the size, Exp[1] is the pointer/address.
            // Note that the invocation of the constructor is a separate call,
            // and so need not be considered here.
            if (isDirectCall(edge, /operator new/) &&
                edge.PEdgeCallInstance.Exp.length == 2 &&
                isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp[1]))
            {
                return true;
            }

            // Coercion via AsAString preserves safety.
            if (isDirectCall(edge, /AsAString/) &&
                isEdgeSafeArgument(entry, edge.PEdgeCallInstance.Exp))
            {
                return true;
            }

            // Special case:
            //
            //   keyframe->mTimingFunction.emplace()
            //   keyframe->mTimingFunction->Init()
            //
            // The object calling Init should be considered safe here because
            // we just emplaced it, though in general keyframe::operator->
            // could do something crazy.
            if (isDirectCall(edge, /operator->/)) do {
                const predges = getPredecessors(body)[edge.Index[0]];
                if (!predges || predges.length != 1)
                    break;
                const predge = predges[0];
                if (!isDirectCall(predge, /\bemplace\b/))
                    break;
                const instance = predge.PEdgeCallInstance;
                if (JSON.stringify(instance) == JSON.stringify(edge.PEdgeCallInstance))
                    return true;
            } while (false);
        }

        if (isSafeAssignment(entry, edge, name))
            return true;

        // Watch out for variables which were assigned arguments.
        var rhsVariable = variableAssignRhs(edge);
        if (rhsVariable)
            return isSafeVariable(entry, rhsVariable);
    }

    // When temporary stack structures are created (either to return or to call
    // methods on without assigning them a name), the generated sixgill JSON is
    // rather strange. The temporary has structure type and is never assigned
    // to, but is dereferenced. GCC is probably not showing us everything it is
    // doing to compile this code. Pattern match for this case here.

    // The variable should have structure type.
    var type = null;
    for (var defvar of currentBody.DefineVariable) {
        if (variableName(defvar.Variable) == name) {
            type = defvar.Type;
            break;
        }
    }
    if (!type || type.Kind != "CSU")
        return false;

    // The variable should not have been written to anywhere up to this point.
    // If it is initialized at this point we should have seen *some* write
    // already, since the CFG edges are visited in reverse post order.
    if (name in assignments)
        return false;

    return true;
}

function isSafeMemberPointer(containerType, memberType)
{
    if (memberType.Kind != 'Pointer')
        return false;

    // Special-cases go here :)
    return false;
}

// Return whether 'exp == value' holds only when execution is on the main thread.
function testFailsOffMainThread(exp, value) {
    switch (exp.Kind) {
      case "Drf":
        var edge = expressionValueEdge(exp.Exp[0]);
        if (edge) {
            if (isDirectCall(edge, /NS_IsMainThread/) && value)
                return true;
            if (isDirectCall(edge, /IsInServoTraversal/) && !value)
                return true;
            if (isDirectCall(edge, /IsCurrentThreadInServoTraversal/) && !value)
                return true;
            if (isDirectCall(edge, /__builtin_expect/))
                return testFailsOffMainThread(edge.PEdgeCallArguments.Exp[0], value);
            if (edge.Kind == "Assign")
                return testFailsOffMainThread(edge.Exp[1], value);
        }
        break;
      case "Unop":
        if (exp.OpCode == "LogicalNot")
            return testFailsOffMainThread(exp.Exp[0], !value);
        break;
      case "Binop":
        if (exp.OpCode == "NotEqual" || exp.OpCode == "Equal") {
            var cmpExp = isZero(exp.Exp[0])
                ? exp.Exp[1]
                : (isZero(exp.Exp[1]) ? exp.Exp[0] : null);
            if (cmpExp)
                return testFailsOffMainThread(cmpExp, exp.OpCode == "NotEqual" ? value : !value);
        }
        break;
      case "Int":
        if (exp.String == "0" && value)
            return true;
        if (exp.String == "1" && !value)
            return true;
        break;
    }
    return false;
}
