3
\$\begingroup\$

I am creating a dialect of ActionScript 3.0 and now have a working verifier, or “symbol solving” verifier. The language upgrades ActionScript 3.0 with few operators, light syntax facilities, numeric enums and way more compile-time semantics.

To test verifying, run:

$ git clone https://github.com/shockscript/sxc
$ cd sxc
$ npm install
$ npm link
$ npm test

There's no proper test unit, but the command npm test runs sxc tests/sources, which includes all sources in sxc/library and few sources in sxc/tests/sources, parses and verifies them. Errors accompanied by the comment // Verify error will occur.

Generic functions are alternatively called multi-functions and do not involve type substitution at compile time:

generic function fn(value):Boolean
generic function fn(value:RegExp):Boolean
generic function fn(value:String):Boolean

Generic classes are final and therefore cannot be extended. The following is an example:

final class A.<T>
{
}
var a = new A.<A.<uint>>

Type substitution only occurs with instantiations of generic classes. It occurs directly with every access on the instantiation. I'm wondering if my substitution with structural types (aka. composed types) is correct: to prevent cycles, it pre-creates type symbols and mutates them later once the further composing types have any type parameter occurrence replaced.

type.js

class Type {
    /*...*/
    replace(params, args) {
        return this._replace(params, args)
    }

    _replace(params, args, w = null, u = null) {
        if (this instanceof ClassType) {
            if (this.typeParameters) {
                const args2 = []
                for (const param of this.typeParameters) {
                    const i = params.indexOf(param)
                    args2.push(i === -1 ? param : args[i])
                }
                return this.instantiate(args2)
            }
        }
        else if (this instanceof GI) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2) return type2
            type2 = new GI(null, [])
            w.set(this, type2)
            const args = []
            for (const type of this.originArguments)
                args.push(type._replace(params, args, w))
            return this.origin.instantiate(args, type2)
        }
        else if (this instanceof TypeParameter) {
            const i = params.indexOf(this)
            if (i !== -1) return args[i]
        }
        else if (this instanceof FunctionType) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2) return type2
            type2 = new FunctionType
            w.set(this, type2)
            if (this.params) {
                type2.params = []
                for (const param of this.params)
                    type2.params.push(param._replace(params, args, w))
            }
            if (this.optParams) {
                type2.optParams = []
                for (const param of this.optParams)
                    type2.optParams.push(param._replace(params, args, w))
            }
            if (this.restParam)
                type2.restParam = this.restParam._replace(params, args, w)
            if (this.returnType)
                type2.returnType = this.returnType._replace(params, args, w)
            type2.delegate = this.context.statics.ObjectType.delegate
            type2.context = this.context
            return type2
        }
        else if (this instanceof TupleType) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2) return type2
            type2 = new TupleType([])
            w.set(this, type2)
            for (const type of this.types)
                type2.types.push(type._replace(params, args, w))
            type2.delegate = this.context.statics.ObjectType.delegate
            type2.context = this.context
            return type2
        }
        else if (this instanceof UnionType) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2) return type2
            type2 = new UnionType([])
            w.set(this, type2)
            u = u || []
            u.push(type2)
            for (const type of this.types) {
                const type3 = type._replace(params, args, w, u)
                if (u.indexOf(type3) === -1 && this.types.indexOf(type3) === -1)
                    type2.types.push(type3)
            }
            type2.context = this.context
            u.pop()
            return type2
        }
        else if (this instanceof NullableType) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2)
                return type2
            type2 = new NullableType(null)
            w.set(this, type2)

            let innerType = this.innerType._replace(params, args, w)
            if (innerType instanceof NullableType)
                if (!innerType.innerType.containsNullValue())
                    innerType = innerType.innerType
            else if (innerType instanceof NonNullableType)
                innerType = innerType.innerType

            type2.innerType = innerType
            type2.delegate = innerType.delegate
            type2.context = this.context
            return type2
        }
        else if (this instanceof NonNullableType) {
            w = w || new Map
            let type2 = w.get(this)
            if (type2)
                return type2

            type2 = new NonNullableType(null)
            w.set(this, type2)

            let innerType = this.innerType._replace(params, args, w)
            if (innerType instanceof NullableType)
                if (innerType.innerType.containsNullValue())
                    innerType = innerType.innerType
            else if (innerType instanceof NonNullableType)
                innerType = innerType.innerType

            type2.innerType = innerType
            type2.delegate = innerType.delegate
            type2.context = this.context
            return type2
        }
        return this
    }
    /*...*/
}

The parameters w and u serve to prevent any cycle. w means wrapping types (given by a offending _replace() call) and u means union hierarchy (to prevent union reoccuring with itself).

\$\endgroup\$
6
  • 1
    \$\begingroup\$ I scanned through the project at GitHub, and I wondered where the test suite is. For a programming language of this complexity I expect a test suite that covers success and failure cases for each of the type comparisons. Right now it looks to me as if your language defines that a NullableType of X equals X, which sounds obviously wrong to me. Where are these tests and examples? \$\endgroup\$ Commented Nov 29, 2019 at 22:39
  • \$\begingroup\$ @RolandIllig I've added a comprovation that this confusion does not occur: nullability.sx. It'll report VerifyError: Error #1317: Parameter must be of type ?uint (for callback that takes ?uint, but explicitly passes uint). A type annotation is not even required to begin with in this case, so it's redundant. \$\endgroup\$ Commented Nov 30, 2019 at 1:23
  • \$\begingroup\$ And where is the corresponding test that ensures that the compiler issues exactly this error message? \$\endgroup\$ Commented Nov 30, 2019 at 4:59
  • \$\begingroup\$ @RolandIllig In case I put instructions in this post. It requires Git and Node.js in your shell environment. If under Termux app (Android) you can install Node.js by simply running pkg install nodejs. After installing ShockScript (entering $ cd shockscript/comp-sx, $ npm install and $ npm link), you can verify all sources under testsuite using the command comp-sx --builtins langlib/**/*.sx testsuite/**/*.sx (you must reside in comp-sx directory as these source directories are located there). \$\endgroup\$ Commented Nov 30, 2019 at 11:25
  • \$\begingroup\$ Does the "you can verify" mean I have to manually inspect the output? If so, that's unacceptable since this is something that is too boring and error-prone for humans. Tasks like this are better left to computers. \$\endgroup\$ Commented Nov 30, 2019 at 11:33

0

You must log in to answer this question.