typemaker-lang / typemaker

Typescript for BYOND's DreamMaker
GNU General Public License v3.0
4 stars 3 forks source link

Typemaker

Typemaker is a language designed to improve scale and maintainibility of BYOND's DreamMaker programming language

Goals

Insert deep philosophy about how /tg/ coders hurt Cyberboss' feelings

No but seriously, I just wanna make the project more maintainable

Features

C-Style Blocks

Statements must end with ;s. Blocks must either use {}s or be a single statement. Entire program may be written on one line

/proc/ThisIsAValidProcDefinition() -> void world << "Hello";

/proc/SoIsThis() -> string {
    return "asdf"
}

Strong Typing

All vars are implicitly prefixed. Manual prefixing is required if type deduction isn't obvious. Casting is only implcit for int -> float. All other usages must match.

Proc parameters forced to follow the type/name syntax without leading slash and are validated at the call site if no default values exist

New prefixes: /resource, /bool, /string, /path, /int, /float, /dict, /interface, /enum. /nullable comes before any prefix if the variable may be null.

/path/concrete limits to non-abstract paths and is the only /path type usable in new statements.

/list by itself is an unsafe type and may only be read in unsafe blocks

/list/<another path> now allows accessing an indexed list strongly

/dict is a /list keyed by something. The format is /dict/<key_type>\\<value_type> They cannot be keyed by /dicts, /bools, /ints, or /floats. Dicts are initialized with the dict() proc, which is identical to the list() proc in dm except all entries must be keyed.

No default initialization for /string and /path

Intermixing ints and floats converts the result type to a float.

All paths must be absolute

Non-nullable var types in datum definitions must be initialized in /New()

Declarative Return Types

/datum/example/proc/Foo() -> /datum/bar {
    //errors if missing or incorrect return statement
}

/atom/proc/MoveLeft() -> /nullable/int {
    if(prob(50))
        return null;
    return 4;
}

/proc/lemon() -> void {

}

Enums

The /enum type cannot be used on it's own and represents a strongly typed set of values. May be freely converted to and from their backing type (int or string). Automatic incrementing int's by default. If strings, value must be declared

/enum/Thing {
    A,  //default 0
    B,  //default 1
    C = 17,
    D   //default 18
}

/enum/StringEnum {
    A = "asdf",
    B = "fdsa"
}

/proc/Example() -> void {
    var/enum/Thing/X = /enum/Thing/C;
    var/int/C = X;
    C += 20;
    //no backcast validation
    X = C;
}

Nameof

nameof() simply takes any identifier and stringifies the most significant portion of it

/datum/foo {
    var/string/asdf = "fdsa";
}

/proc/Example -> void {
    var/string/X = nameof(/proc); //"proc"
    X = nameof(/datum/foo/proc/Example); //"Example"
    X = nameof(asdf); //"asdf"
}

## Access Modifiers

### Static Procs

These compile to global procs

```dm
static /datum/test/proc/Foo() -> void {
    //src is not avaiable here
}

/world/New() -> void {
    //invoke as so
    /datum/test.Foo();
}

Public, Protected, Readonly

All Typemaker accesses default to private. All DreamMaker access defaults to public

/proc/example() -> void {} //global procs public by default and cannot be decorated

/datum/test
{
    var/int/only_accessible_by_test = 1;
    readonly var/int/can_only_be_changed_in_constructor = 5;
    protected var/int/only_accessible_by_test_and_children = 2;
    public var/int/accessible_by_everyone = 3;
}

/datum/test/New() -> void {
    ..();
    can_only_be_changed_in_constructor = 7;
}

public /datum/test/proc/ThisCanBeCalledByAnyone() -> void {}
protected /datum/test/proc/ThisCanOnlyBeCalledByTestOrChildren -> void {}
/datum/test/proc/ThisCanOnlyBeCalledByTest -> void {}

Partial, Sealed

Datum definition block must occur before all proc definitions for said datum in a file

Datums with variable definitions in more than one block or proc definitions in more than one file must be declared as partial.

/datum/this_can_be_inherited {}
//procs for /datum/this_can_be_inherited cannot be defined before here

sealed /datum/this_can_be_inherited/but_this_cannot {}

sealed partial /datum/example_partial {
    var/int/i = 4;
}

partial /datum/example_partial {
    //sealed does not have to be redeclared
    var/int/j = 5;
}

Virtual, Abstract, Final

Procs are no longer allowed to be considered virtual by default except in DreamMaker code

Abstract procs requires non-abstract child types to override the implementation.

Abstract can be applied at proc or datum level, both makes entirety of datum abstract. Abstract datums cannot be directly instantiated and their types cannot be used in /path/concrete

Datums can be sealed to prevent further inheritance

Arguments must be maintained by overrides. Default arguments must come last

Overridden procs may remove the /nullable spec from return types.

Virtual/abstract procs must be public or protected

New() is the only virtual proc that may have it's arguments changed by children

/datum/foo/proc/CannotBeOverridden() -> void {}

/datum/foo/New(int/first_arg) -> void {
    ..()
}

protected virtual /datum/foo/proc/CanBeOverridden(datum/enforced_on_children) -> nullable/int {}

public abstract /datum/foo/proc/MustBeOverridden(int/x, datum/enforced_on_children = null) -> void;

/datum/foo/bar/New(string/can_have_different_args_than_parent) -> void {
    //but parent must still be called with correct args if at all
    if(prob(50))
        ..(4);
}

/datum/foo/bar/CanBeOverridden() -> int {
    ..(); //not necessary
    return 4;
}

final /datum/foo/bar/MustBeOverridden(int/x = 4, datum/enforced_on_children = new) -> void {
    //cannot be overridden again
}

Interfaces

/interface is a declarative only type that describes a set of public variables and procs a non-abstract datum must implement. Datums that implement interfaces are implicitly castable to interface vars of that type.

/interface paths cannot be used as literals

var/interface/x; is an invalid variable declaration.

interfaces use multiple inheritance and only have one identifier

implements must be in a declaration block of a datum or interface to bind it to the contract

/interface/IEmptyInterfacesAreValidAndStillTypeChecked {}

/interface/IExtendedExample
{
    implements IExample;

    var/string/must_have_this_public_var;
}

/interface/IExample
{
    proc/MustHaveThisPublicProcWithThisSignature(int/x) -> void;
}

/datum/example {
    implements IExample/IExtendedExample;
    var/string/must_have_this_public_var;
}

/proc/InterfaceParameterAcceptanceExample(nullable/interface/IExample) -> void {}

public /datum/foo/bar/proc/MustHaveThisPublicProcWithThisSignature(int/x) -> void {
    InterfaceParameterAcceptanceExample(null);
    InterfaceParameterAcceptanceExample(src);
}

abstract /datum/foo {
    implements IExample;
    implements IEmptyInterfacesAreValidAndStillTypeChecked

    //abstract datums don't need to implement entire/any of interfaces
    var/string/must_have_this_public_var;
}

/datum/foo/bar {
    //cannot redeclare inherited implements
}

//virtual/abstract allowed
public virtual /datum/foo/bar/proc/MustHaveThisPublicProcWithThisSignature(int/x) -> void {}

Order-Free Compilation

Remove macros entirely, hide quirks that make code inclusion order matter. Transpiled macros will be uniquely named to prevent namespace pollution

True Const Variables

var/const/*/Varname optimized to #define _<UNIQUE>_Varname at the "cost" of removing them from /datum.vars. Still scoped appropriately

Macro Inlining

The inline decorator marks a function to be compiled into wherever it is called. All procs may be inline. An inline datum makes all it's functions inline and implements variables (if any) as a list() in generated code.


inline /datum/gas_mixture/proc/assert_gas(path/gas_path) -> void {
    //src is valid

    //you know where i'm going with this
}

//globals of course can be inline too
inline /proc/BoldAnnounce() -> void {
    world << "This runs inline wherever it's called";
}

Override Precedence

All functions have an override precedence which defaults to zero

When overriding the same proc more than once or decalaring and overriding the same proc in a datum, execution order is determined via override precedence from highest to lowest

It is a compilation error 2 or more of the same overrides in one path have the same precedence

Override precedence can be set with the precedence() decorator

Only virtual/abstract functions can have precedence like this


precedence(-1) /datum/foo/Bar() -> void {
    //this will not be called
}

virtual precedence(1) /datum/foo/proc/Bar() -> void {
    //this will be run 1st when called
    ..()
}

//precedence(0)
/datum/foo/Bar() -> void {
    //this will be run second when called
}

.dmm Inclusion

.dmm files are now included via the top level include_map() directive

include_map('_maps/BoxStation/BoxStation.dmm')

DreamMaker Compatibility

Option to include a .dme which will be the prefix for the output .dme the compiler genenerates.

If done, the unsafe block is unlocked to allow assigning from and calling into DM written code

arglist() and call()() cannot be used outside of unsafe blocks

/proc/dm_access_example() -> void {
    var/int/test
    var/int/test2
    unsafe {
        //typechecking stopped for this block
        var/datum/dm_declared_datum/D = new
        test = D.Func()
    }

    //test now assumed to be valid
    //test2 still unassigned
}

Declarations

Declarations allow strong typing of existing DM types/var/functions without defining their values or bodies. These are used to expose the DM standard library to Typemaker code. Static and non-virtual procs cannot be declared (the virtual is implied). Untyped declarations cannot be used outside of unsafe blocks

foo.dm

/proc/Something() 
    world.log << "Hello world";

/datum/foo/var/whatever = list();
/datum/foo/var/whatever2 = "asdf";

/datum/foo/proc/Run()
    return 4

foo.tm

declare /proc/Something() -> void;

declare /datum/foo {
    public var/unknown_type_can_only_be_used_in_unsafe_block;

    //only public and protected allowed
    protected var/string/whatever2;

    //when declaring /New overrides, omit /proc
    protected /New(string/asdf);

    //whatever can't be accessed by typemaker
    public /proc/Run() -> int;
}

bar.tm

/proc/bar() -> void {
    var/datum/foo = new ("fdsa");
    foo.Run();

    var/string/val;
    unsafe {
        val = foo.unknown_type_can_only_be_used_in_unsafe_block;
        foo.unknown_type_can_only_be_used_in_unsafe_block = 42;
    }
}

Other Proc Decorators

async

The async proc decorator is equivalent to adding set waitfor = FALSE; to the first line of your proc

async /proc/foo() -> void {}

is equivalent to

/proc/foo()
    set waitfor = FALSE

You should prefer async as it allows the compiler to anaylze correctly. Async methods must return void

entrypoint

The entrypoint proc decorator tells the analyzer this code can be invoked at the start of a new thread by the runtime.

/client/New()   //libdm functions have appropriate `entrypoint`s specified in their declarations
{
    ..();
    verbs += /datum/proc/foo;
}

//tells the compiler to anaylze static paths from this point
entrypoint /datum/proc/foo(int/count) -> void {
    set name = "Woot"
    for(var I in 1 to count)
        world.log << "woot!\n";
}

yield

The yield decorator is valid only for proc declarations and indicates that calling the proc will put the current "thread" to sleep. This is used for static analysis

i.e. The libdm declaration of some functions

declare yield /proc/sleep(float/ticks) -> void;

declare /world {
    yield proc/Export(string/Addr, nullable/file/File = null, nullable/bool/Persist = null, nullable/list/client/Clients = null);
}

Optimization

Transpiled code will use : access operators wherever possible. Code transpiled as relatively pathed for compiler optimization. Unreferenced code will be eliminated (.vars usage does not prevent this)

Explicit keyword

The explicit keyword keeps variables/datum/functions from undergoing dead code elimination. Use this when these are valid reflection types. declared types and unsafe blocks propagate explicitness.

explicit /datum/example {
    //none of these vars, procs, or the datum will be optimized out
    var/int/x = 4;
}

/datum/some_things_eliminated {
    var/int/wont_be_elimiated_because_of_proc_foo = 4;
    var/int/wont_be_elimiated_because_of_proc_WontBeEliminated = 4;

    var/nullable/string/this_will_be_eliminated;
    explicit var/nullable/string/this_wont_be_elimiated;
}

/datum/some_things_eliminated/proc/WillBeElminated() -> void {}

explicit /datum/some_things_eliminated/proc/WontBeElminated() -> void {
    wont_be_elimiated_because_of_proc_WontBeEliminated = 5;
}

/datum/some_things_eliminated/proc/WontBeElminatedBecauseOfProcFoo() -> void {}

//won't be eliminated
explicit /proc/foo() -> void {
    var/datum/some_things_elimiated/D = ;
    D.wont_be_elimiated_because_of_proc_foo = 5;
    D.WontBeElminatedBecauseOfProcFoo();
}

Cross platform, easy installation

Single binary installer, ideally workable via shell command ala rust. Installs to ~/.typemaker/bin, multiple .so/.dlls fine provided no system dependencies required. Overwrite for updates. Delete to uninstall.

Compiler named tmc

BYOND Version Management

Projects specify exactly which BYOND version to use for compilation in typemaker.json file. tmc handles downloading and installing versions in ~/.typemaker/byond as necessary.

Other Executables

tm_langserv is a langserver protocol executable

tm_edit launches DreamMaker's icon and map editor for all .dmi and .dmm files in the code tree after transpiling

tm_vm handles upgrading, downgrading, and uninstalling the typemaker installation

Folder Based Compilation

Automatically include all .tm files in a project root

File Restrictions

There is a standard declaration order that must be followed for Typemaker files:

  1. Map declarations

  2. Declared Global Variables

  3. Global Variables

  4. Enums and Interfaces

  5. Declared Global Procs

  6. Global Procs

  7. Declared Datums

  8. True Datum Declarations

  9. Datum Proc Definitions

typemaker.json Format

{
    "version": "<file schema semver>",
    "extends": "<optional path to additional json file, results will be merged, does not change default code root>"
    "library": "<true/false>, default false, if this represents a library to be included in a final output",
    "libraries": [
        "Additional library typemaker.jsons to include, TODO",
        "Appropriate libdm is automatically included"
    ],
    "include": {
        "root": "<optionally specify a directory other than `.`>",
        "ignore": [
            "list of",
            "paths to",
            "ignore"
        ]
    },
    "output_directory": "<optionally specify a directory other than `.`, not valid for libraries>",
    "byond_version": "<A version definitions object>",
    "strong_libdm": "<true/false>, default true. If false, the dm standard library will use protected/private/final/abstract/sealed variables, procs, and objects where appropriate",
    "debug": "<true/false, default true, sets the DEBUG preprocessor directive, not valid for libraries>",
    "scripts":
    {
        "pre_transpile": "<Shell command to invoke before beginning transpilation>",
        "pre_compile": "<Shell command to invoke before running dm>",
        "post_compile": "<Shell command to invoke after successfully running dm>"
    },
    "dme": "<Path to .dme to #include before transpiled code>, not valid for libraries",
    "linter_settings": {
        "enforce_tabs": "<optional true/false, false enforces spaces, default non-enforce>",
        "allman_braces": "<optional true/false, false enforces BSD KNF, default non-enforce>",
        "no_operator_overloading": "<true/false, default true>",
        "no_single_line_blocks": "<true/false, default false>",
        "other_options": "to come"
    }
}

Version Definitions

{
    "min": "libraries only, a Byond Version object specifiying the minimum supported BYOND version",
    "max": "libraries only, a Byond Version object specifiying the maximum supported BYOND version",
    "target": "not for libraries, a Byond Version object specifiying the target BYOND version"
}

Shell Command

{
    windows: "<Path to .bat or ps1 file>",
    linux: "<Path to .sh file>"
}

Feedback

Please leave feedback here