Typemaker is a language designed to improve scale and maintainibility of BYOND's DreamMaker programming language
Insert deep philosophy about how /tg/ coders hurt Cyberboss' feelings
No but seriously, I just wanna make the project more maintainable
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"
}
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 /dict
s, /bool
s, /int
s, or /float
s. 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 int
s and float
s converts the result type to a float.
All paths must be absolute
Non-nullable var types in datum definitions must be initialized in /New()
/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 {
}
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()
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();
}
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 {}
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;
}
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
}
/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 {}
Remove macros entirely, hide quirks that make code inclusion order matter. Transpiled macros will be uniquely named to prevent namespace pollution
var/const/*/Varname
optimized to #define _<UNIQUE>_Varname
at the "cost" of removing them from /datum.vars
. Still scoped appropriately
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";
}
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
files are now included via the top level include_map()
directive
include_map('_maps/BoxStation/BoxStation.dmm')
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 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;
}
}
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
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";
}
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);
}
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)
The explicit keyword keeps variables/datum/functions from undergoing dead code elimination. Use this when these are valid reflection types. declare
d 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();
}
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
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.
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
Automatically include all .tm
files in a project root
There is a standard declaration order that must be followed for Typemaker files:
-
Map declarations
-
Declared Global Variables
-
Global Variables
-
Enums and Interfaces
-
Declared Global Procs
-
Global Procs
-
Declared Datums
-
True Datum Declarations
-
Datum Proc Definitions
{
"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"
}
}
{
"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"
}
{
windows: "<Path to .bat or ps1 file>",
linux: "<Path to .sh file>"
}
Please leave feedback here