Dascript
Dascript
Dascript
Anton Yudintsev
1 Introduction 3
1.1 Performance. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 How it looks? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.3 Generic programming and type system . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.4 Compilation time macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
1.5 Features . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5
2 The language 7
2.1 Lexical Structure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.1 Identifiers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.2 Keywords . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7
2.1.3 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.1.4 Other tokens . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.1.5 Literals . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
2.1.6 Comments . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.1.7 Semantic Indenting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2 Values and Data Types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
2.2.1 Integer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.2 Float . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.3 Bool . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.4 String . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
2.2.5 Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.6 Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.7 Struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.8 Classes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.9 Variant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
2.2.10 Tuple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.11 Enumeration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.12 Bitfield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.13 Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.14 Reference . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
2.2.15 Pointers . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.2.16 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3 Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.1 Visibility Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.2 Control Flow Statements . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
2.3.3 Ranged Loops . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
2.3.4 break . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.5 continue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
2.3.6 return . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
i
2.3.7 yield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.8 Finally statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
2.3.9 Local variables declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3.10 Function declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
2.3.11 try/recover . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.12 panic . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.13 global variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.14 enum . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.3.15 Expression statement . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
2.4 Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.1 Assignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
2.4.2 Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20
2.4.3 Array Initializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.4.4 Struct, Class, and Handled Type Initializer . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.5 Tuple Initializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.6 Variant Initializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
2.4.7 Table Initializer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.5 Temporary types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2.6 Built-in Functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.1 Invoke . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.6.2 Misc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.7 Clone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.7.1 Cloning rules and implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.7.2 clone_to_move implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.8 Unsafe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.9 Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.10 Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.11 Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.11.1 Function declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.11.2 OOP-style calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.11.3 Tail Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.12 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.12.1 Native modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.12.2 Builtin modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.12.3 Shared modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.12.4 Module function visibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.13 Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.14 Lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.14.1 Capture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.14.2 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.14.3 Implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.15 Struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.15.1 Struct Declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.15.2 Structure Function Members . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.15.3 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.15.4 Alignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.15.5 OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.16 Tuple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.17 Variant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.17.1 Alignment and data layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.18 Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.18.1 Implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.19 Constants, Enumerations, Global variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.19.1 Constant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
ii
2.19.2 Global variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.19.3 Enumeration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.20 Bitfield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.21 Comprehension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.22 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.22.1 builtin iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.22.2 builtin iteration functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.22.3 low level builtin iteration functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.22.4 next implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.23 Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.23.1 implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.24 Finalizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.24.1 Rules and implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.25 String Builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.26 Generic Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.26.1 typeinfo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.26.2 auto and auto(named) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.27 Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.27.1 Compilation passes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
2.27.2 Invoking macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.27.3 AstFunctionAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.27.4 AstBlockAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.27.5 AstStructureAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.27.6 AstEnumerationAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.27.7 AstVariantMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.27.8 AstReaderMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
2.27.9 AstCallMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.27.10 AstPassMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.27.11 AstTypeInfoMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
2.27.12 AstForLoopMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
2.27.13 AstCaptureMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
2.27.14 AstCommentReader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.27.15 AstSimulateMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.27.16 AstVisitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.28 Reification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.28.1 Simple example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.28.2 Quote macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.28.3 Escape sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.29 Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
2.29.1 Initialization and shutdown . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.29.2 Macro contexts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.29.3 Locking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.29.4 Lookups . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.30 Locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.30.1 Context locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.30.2 Array and Table locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83
2.30.3 Array and Table lock checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
iii
3.2.2 ModuleAotType . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.2.3 Builtin module constants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.2.4 Builtin module enumerations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
3.2.5 Builtin module data types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
3.2.6 Builtin module macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
3.2.7 Builtin module functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
3.2.8 Function side-effects . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.2.9 File access . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
3.2.10 Project . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.3 C++ ABI and type factory infrastructure . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.3.1 Cast . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
3.3.2 Type factory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.3.3 Type aliases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
3.4 Exposing C++ handled types . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3.4.1 TypeAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3.4.2 ManagedStructureAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.4.3 DummyTypeAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
3.4.4 ManagedVectorAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.4.5 ManagedValueAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.5 Ahead of time compilation and C++ operation bindings . . . . . . . . . . . . . . . . . . . . . . . . 99
3.5.1 das_index . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
3.5.2 das_iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
3.5.3 AOT template function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
3.5.4 AOT settings for individual functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.5.5 AOT prefix and suffix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101
3.5.6 AOT field prefix and suffix . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102
iv
daScript Reference Manual, Release 0.2 beta
CONTENTS 1
daScript Reference Manual, Release 0.2 beta
2 CONTENTS
CHAPTER
ONE
INTRODUCTION
daScript is a high-performance, strong and statically typed scripting language, designed to be high-performance as an
embeddable “scripting” language for real-time applications (like games).
daScript offers a wide range of features like strong static typing, generic programming with iterative type inference,
Ruby-like blocks, semantic indenting, native machine types, ahead-of-time “compilation” to C++, and fast and sim-
plified bindings to C++ program.
It’s philosophy is build around a modified Zen of Python.
• Performance counts.
• But not at the cost of safety.
• Unless is explicitly unsafe to be performant.
• Readability counts.
• Explicit is better than implicit.
• Simple is better than complex.
• Complex is better than complicated.
• Flat is better than nested.
daScript is supposed to work as a “host data processor”. While it is technically possible to maintain persistent state
within a script context (with a certain option set), daScript is designed to transform your host (C++) data/implement
scripted behaviors.
In a certain sense, it is pure functional - i.e. all persistent state is out of the scope of the scripting context, and the script’s
state is temporal by its nature. Thus, the memory model and management of persistent state are the responsibility of
the application. This leads to an extremely simple and fast memory model in daScript itself.
1.1 Performance.
In a real world scenarios, it’s interpretation is 10+ times faster than LuaJIT without JIT (and can be even faster than
LuaJIT with JIT). Even more important for embedded scripting languages, its interop with C++ is extremely fast (both-
ways), an order of magnitude faster than most other popular scripting languages. Fast calls from C++ to daScript allow
you to use daScript for simple stored procedures, and makes it an ECS/Data Oriented Design friendly language. Fast
calls to C++ from daScript allow you to write performant scripts which are processing host (C++) data, and rely on
bound host (C++) functions.
It also allows Ahead-of-Time compilation, which is not only possible on all platforms (unlike JIT), but also always
faster/not-slower (JIT is known to sometimes slow down scripts).
3
daScript Reference Manual, Release 0.2 beta
daScript already has implemented AoT (C++ transpiler) which produces code more or less similar with C++11 per-
formance of the same program.
Table with performance comparisons on a synthetic samples/benchmarks.
def fibR(n)
if (n < 2)
return n
else
return fibR(n - 1) + fibR(n - 2)
def fibI(n)
var last = 0
var cur = 1
for i in range(0, n - 1)
let tmp = cur
cur += last
last = tmp
return cur
The same samples with curly brackets, for those who prefer this type of syntax:
def fibR(n) {
if (n < 2) {
return n;
} else {
return fibR(n - 1) + fibR(n - 2);
}
}
def fibI(n) {
var last = 0;
var cur = 1;
for i in range(0, n-1); {
let tmp = cur;
cur += last;
last = tmp;
}
return cur;
}
Please note, that semicolons(‘;’) are mandatory within curly brackets. You can actually mix both ways in your codes,
but for clarity in the documentation, we will only use the pythonic way.
4 Chapter 1. Introduction
daScript Reference Manual, Release 0.2 beta
Although above sample may seem to be dynamically typed, it is actually generic programming. The actual instance
of the fibI/fibR functions is strongly typed and basically is just accepting and returning an int. This is similar to
templates in C++ (although C++ is not a strong-typed language) or ML. Generic programming in daScript allows very
powerful compile-time type reflection mechanisms, significantly simplifying writing optimal and clear code. Unlike
C++ with it’s SFINAE, you can use common conditionals (if) in order to change the instance of the function depending
on type info of its arguments. Consider the following example:
This function sets someField in the provided argument if it is a struct with a someField member.
(For more info, see Generic programming).
daScript does a lot of heavy lifting during compilation time so that it does not have to do it at run time. In fact, the
daScript compiler runs the daScript interpreter for each module and has the entire AST available to it.
The following example modifies function calls at compilation time to add a precomputed hash of a constant string
argument:
[tag_function_macro(tag="get_hint_tag")]
class GetHintFnMacro : AstFunctionAnnotation
[unsafe] def override transform ( var call : smart_ptr<ExprCall>;
var errors : das_string ) : ExpressionPtr
if call.arguments[1] is ExprConstString
let arg2 = reinterpret<ExprConstString?>(call.arguments[1])
var mkc <- new [[ExprConstUInt() at=arg2.at, value=hash("{arg2.value}")]]
push(call.arguments, ExpressionPtr(mkc))
return <- ExpressionPtr(call)
return [[ExpressionPtr]]
1.5 Features
6 Chapter 1. Introduction
CHAPTER
TWO
THE LANGUAGE
2.1.1 Identifiers
Identifiers start with an alphabetic character (and not the symbol ‘_’) followed by any number of alphabetic characters,
‘_’ or digits ([0-9]). daScript is a case sensitive language meaning that the lowercase and uppercase representation of
the same alphabetic character are considered different characters. For instance, “foo”, “Foo” and “fOo” are treated as
3 distinct identifiers.
2.1.2 Keywords
The following words are reserved as keywords and cannot be used as identifiers:
The following words are reserved as type names and cannot be used as identifiers:
7
daScript Reference Manual, Release 0.2 beta
2.1.3 Operators
+= -= /= *= %= |= ^= <<
>> ++ -- <= <<= >>= >= ==
!= -> <- ?? ?. ?[ <| |>
:= <<< >>> <<<= >>>= => + @@
- * / % & | ^ >
< ! ~ && || ^^ &&= ||=
^^=
{ } [ ] . :
:: ' ; " ]] [[
[{ }] {{ }} @ $
#
2.1.5 Literals
daScript accepts integer numbers, unsigned integers, floating and double point numbers and string literals.
Pesudo BNF:
2.1.6 Comments
A comment is text that the compiler ignores, but is useful for programmers. Comments are normally used to embed
annotations in the code. The compiler treats them as white space.
A comment can be /* (slash, asterisk) characters, followed by any sequence of characters (including new lines),
followed by the */ characters. This syntax is the same as ANSI C:
/*
This is
a multiline comment.
This lines will be ignored by the compiler.
*/
A comment can also be // (two slash) characters, followed by any sequence of characters. A new line not immediately
preceded by a backslash terminates this form of comment. It is commonly called a “single-line comment”:
// This is a single line comment. This line will be ignored by the compiler.
daScript follows semantic indenting (much like Python). That means that logical blocks are arranged with the same
indenting, and if a control statement requires the nesting of a block (such as the body of a function, block, if, for, etc.),
it has to be indented one step more. The indenting step is part of the options of the program. It is either 2, 4 or 8, but
always the same for whole file. The default indenting is 4, but can be globally overridden per project.
daScript is a strong, statically typed language. All variables have a type. daScript’s basic POD (plain old data) data
types are:
All PODs are represented with machine register/word. All PODs are passed to functions by value.
daScript’s storage types are:
They can’t be manipulated, but can be used as storage type within structs, classes, etc.
daScript’s other types are:
2.2.1 Integer
2.2.2 Float
let a = 1.0
let b = 0.234
let a = float2(1.0, 2.0)
2.2.3 Bool
A bool is a double-valued (Boolean) data type. Its literals are true and false. A bool value expresses the validity
of a condition (tells whether the condition is true or false):
let a = true
let b = false
All conditionals (if, elif, while) work only with the bool type.
2.2.4 String
Strings are an immutable sequence of characters. In order to modify a string, it is necessary to create a new one.
daScript’s strings are similar to strings in C or C++. They are delimited by quotation marks(") and can contain escape
sequences (\t, \a, \b, \n, \r, \v, \f, \\, \", \', \0, \x<hh>, \u<hhhh> and \U<hhhhhhhh>):
Strings type can be thought of as a ‘pointer to the actual string’, like a ‘const char *’ in C. As such, they will be passed
to functions by value (but this value is just a reference to the immutable string in memory).
das_string is a mutable string, whose content can be changed. It is simply a builtin handled type, i.e., a std::string
bound to daScript. As such, it passed as reference.
2.2.5 Table
(see Tables).
2.2.6 Array
Arrays are simple sequences of objects. There are static arrays (fixed size) and dynamic arrays (container, size is
dynamic). The index always starts from 0:
(see Arrays).
2.2.7 Struct
Structs are records of data of other types (including structs), similar to C. All structs (as well as other non-POD types,
except strings) are passed by reference.
(see Structs).
2.2.8 Classes
Classes are similar to structures, but they additionally allow built-in methods and rtti.
(see Classes).
2.2.9 Variant
Variant is a special anonymous data type similar to a struct, however only one field exists at a time. It is possible to
query or assign to a variant type, as well as the active field value.
(see Variants).
2.2.10 Tuple
Tuples are anonymous records of data of other types (including structs), similar to a C++ std::tuple. All tuples (as well
as other non-POD types, except strings) are passed by reference.
(see Tuples).
2.2.11 Enumeration
An enumeration binds a specific integer value to a name, similar to C++ enum classes.
(see Enumerations).
2.2.12 Bitfield
Bitfields are an anonymous data type, similar to enumerations. Each field explicitly represents one bit, and the storage
type is always a uint. Queries on individual bits are available on variants, as well as binary logical operations.
(see Bitfields).
2.2.13 Function
However, there are generic (templated) functions, which will be ‘instantiated’ during function calls by type inference:
def twice(a)
return a + a
(see Functions).
2.2.14 Reference
References are types that ‘reference’ (point to) some other data:
2.2.15 Pointers
Pointers are types that ‘reference’ (point to) some other data, but can be null (point to nothing). In order to work with
actual value, one need to dereference it using the dereference or safe navigation operators. Dereferencing will panic if
a null pointer is passed to it. Pointers can be created using the new operator, or with the C++ environment.
struct Foo
x: int
2.2.16 Iterators
Iterators are a sequence which can be traversed, and associated data retrieved. They share some similarities with C++
iterators.
(see Iterators).
2.3 Statements
Statements in daScript are comparable to those in C-family languages (C/C++, Java, C#, etc.): there are assignments,
function calls, program flow control structures, etc. There are also some custom statements like blocks, structs, and
initializers (which will be covered in detail later in this document). Statements can be separated with a new line or ‘;’.
daScript implements the most common control flow statements: if, while, for
2.3. Statements 13
daScript Reference Manual, Release 0.2 beta
daScript has a strong boolean type (bool). Only expressions with a boolean type can be part of the condition in control
statements.
if/elif/else statement
stat ::= 'if' exp '\n' visibility_block (['elif' exp '\n' visibility_block])* ['else
˓→' '\n' visibility_block]
while statement
for
Executes a loop body statement for every element/iterator in expression, in sequenced order:
for i in range(0, 10)
print("{i}") // will print numbers from 0 to 9
// or
// or
var a: array<int>
var b: int[10]
resize(a, 4)
(continues on next page)
// or
2.3.4 break
2.3.5 continue
The continue operator jumps to the next iteration of the loop, skipping the execution of the rest of the statements.
2.3.6 return
The return statement terminates the execution of the current function, block, or lambda, and optionally returns the
result of an expression. If the expression is omitted, the function will return nothing, and the return type is assumed
to be void. Returning mismatching types from same function is an error (i.e., all returns should return a value of the
same type). If the function’s return type is explicit, the return expression should return the same type.
Example:
def foobar(a)
return a // return type will be same as argument type
2.3. Statements 15
daScript Reference Manual, Release 0.2 beta
In generator blocks, return must always return boolean expression, where false indicates end of generation.
‘return <- exp’ syntax is for move-on-return:
def make_array
var a: array<int>
a.resize(10) // fill with something
return <- a // return will return
2.3.7 yield
Finally declares a block which will be executed once for any block (including control statements). A finally block can’t
contain break, continue, or return statements. It is designed to ensure execution after ‘all is done’. Consider
the following:
require daslib/defer
def foo
(continues on next page)
def bar
defer <|
print("b\n")
print("a\n")
In the example above, functions foo and bar are semantically identical. Multiple defer statements occur in reverse
order.
The defer_delete macro adds a delete statement for its argument, and does not require a block.
Local variables can be declared at any point in a function. They exist between their declaration and the end of the
visibility block where they have been declared. let declares read only variables, and var declares mutable (read-
write) variables.
Copy =, move ->, or clone := semantics indicate how the variable is to be initialized.
def hello
print("hello")
2.3. Statements 17
daScript Reference Manual, Release 0.2 beta
2.3.11 try/recover
The try statement encloses a block of code in which a panic condition can occur, such as a fatal runtime error or a
panic function. The try-recover clause provides the panic-handling code.
It is important to understand that try/recover is not correct error handling code, and definitely not a way to implement
control-flow. Much like in the Go language, this is really an invalid situation which should not normally happen in a
production environment. Examples of potential exceptions are dereferencing a null pointer, indexing into an array out
of bounds, etc.
2.3.12 panic
Calling panic causes a runtime exception with string-exp available in the log.
Declares a constant global variable. This variable is initialized once during initialization of the script (or each time
when script init is manually called).
shared indicates that the constant is to be initialized once, and its memory is shared between multiple instances of
the daScript context.
private indicates that the variable is not visible outside of its module.
2.3.14 enum
In daScript every expression is also allowed to be a statement. If so, the result of the expression is thrown away.
2.4 Expressions
2.4.1 Assignment
a = 10
“Move” assignment:
Move assignment nullifies source (b). It’s main purpose is to correctly move ownership, and optimize copying if you
don’t need source for heavy types (such as arrays, tables). Some external handled types can be non assignable, but still
moveable.
Move assignment is equivalent of C++ memcpy + memset operations:
“Clone” assignment:
a := b
Clone assignment is syntactic sugar for calling clone(var a: auto&; b: auto&) if it exists or basic assignment for POD
types. It is also implemented for das_string, array and table types, and creates a ‘deep’ copy.
Some external handled types can be non assignable, but still cloneable (see Clone).
2.4. Expressions 19
daScript Reference Manual, Release 0.2 beta
2.4.2 Operators
.. Operator
?: Operator
This conditionally evaluate an expression depending on the result of an expression. If expr_cond is true, only exp1
will be evaluated. Similarly, if false, only exp2.
?? Null-coalescing operator
Conditionally evaluate exp2 depending on the result of exp1. The given code is equivalent to:
exp := (exp1 '!=' null) '?' *exp1 ':' exp2
It evaluates expressions until the first non-null value (just like | operators for the first ‘true’ one).
Operator precedence is also follows C# design, so that ?? has lower priority than |
If the value is not null, then dereferences the field ‘key’ for struct, otherwise returns null.
struct TestObjectFooNative
fooData : int
struct TestObjectBarNative
fooPtr: TestObjectFooNative?
barData: float
def test
var a: TestObjectFooNative?
var b: TestObjectBarNative?
var idummy: int
var fdummy: float
a?.fooData ?? idummy = 1 // will return reference to idummy, since a is null
assert(idummy == 1)
a = new TestObjectFooNative
a?.fooData ?? idummy = 2 // will return reference to a.fooData, since a is now
˓→not null
b = new TestObjectBarNative
b?.fooPtr?.fooData ?? idummy = 3 // will return reference to idummy, since while
˓→b is not null, but b.?barData is still null
assert(idummy == 3)
b.fooPtr <- a
b?.fooPtr?.fooData ?? idummy = 4 // will return reference to b.fooPtr.fooData
assert(b.fooPtr.fooData == 4 & idummy == 3)
It checks both the container pointer and the availability of the key.
Arithmetic
daScript supports the standard arithmetic operators +, -, *, / and %. It also supports compact operators +=,
-=, *=, /=, %= and increment and decrement operators ++ and --:
a += 2
// is the same as writing
a = a + 2
x++
// is the same as writing
x = x + 1
All operators are defined for numeric and vector types, i.e (u)int* and float* and double.
Relational
Relational operators in daScript are : ==, <, <=, >, >=, !=.
These operators return true if the expression is false and a value different than true if the expression is true.
Logical
Logical operators in daScript are : &&, ||, ^^, !, &&=, ||=, ^^=.
The operator && (logical and) returns false if its first argument is false, or otherwise returns its second argument. The
operator || (logical or) returns its first argument if is different than false, or otherwise returns the second argument.
The operator ^^ (logical exclusive or) returns true if arguments are different, and false otherwise.
2.4. Expressions 21
daScript Reference Manual, Release 0.2 beta
It is important to understand, that && and || will not necessarily ‘evaluate’ all their arguments. Unlike their C++
equivalents, &&= and ||= will also cancel evaluation of the right side.
The ‘!’ (negation) operator will return false if the given value was true, or false otherwise.
Bitwise Operators
daScript supports the standard C-like bitwise operators &, |, ^, ~, <<, >>, <<<, >>>. Those operators
only work on integer values.
Pipe Operators
daScript supports pipe operators. Pipe operators are similar to ‘call’ expressions where the other expression is first
argument.
def addX(a, b)
assert(b == 2 || b == 3)
return a + b
def test
let t = 12 |> addX(2) |> addX(3)
assert(t == 17)
return true
def addOne(a)
return a + 1
def test
let t = addOne() <| 2
assert(t == 3)
require daslib/lpipe
def main
print()
lpipe() <| "this is string constant"
In the example above, the string constant will be piped to the print expression on the previous line. This allows piping
of multiple blocks while still using significant whitespace syntax.
Operators precedence
2.4. Expressions 23
daScript Reference Manual, Release 0.2 beta
struct Foo
x: int = 1
y: int = 2
let aArray = [[Foo() x=11,y=22; x=33; y=44]] // array of Foo with 'construct'
˓→syntax
Classes and handled (external) types can also be initialized using structure initialization syntax. Classes and handled
types always require constructor syntax, i.e. ().
(see Structs, Classes, Handles ).
(see Tuples).
variant Foo
i : int
f : float
(see Variants).
Tables are created by specifying key => value pairs separated by semicolon:
var a <- {{ 1=>"one"; 2=>"two" }}
var a <- {{ 1=>"one"; 2=>2 }} // error, type mismatch
(see Tables).
Temporary types are designed to address lifetime issues of data, which are exposed to daScript directly from C++.
Let’s review the following C++ example:
void peek_das_string(const string & str, const TBlock<void,TTemporary<const char *>> &
˓→ block, Context * context) {
vec4f args[1];
args[0] = cast<const char *>::from(str.c_str());
context->invoke(block, args, nullptr);
}
The C++ function here exposes a pointer a to c-string, internal to std::string. From daScript’s perspective, the declara-
tion of the function looks like this:
def peek ( str : das_string; blk : block<(arg:string#):void> )
Temporary values can’t be returned or passed to functions, which require regular values:
def accept_string(s:string)
print("s={s}\n")
Values need to be marked as implicit to accept both temporary and regular values. These functions implicitly
promise that the data will not be cached (copied, moved) in any form:
def foo
var a = 13
...
var b = safe_addr(a) // b is int?#, and this operation does not require unsafe
...
Builtin functions are function-like expressions that are available without any modules. They implement inherent mech-
anisms of the language, in available in the AST as separate expressions. They are different from standard functions
(see built-in functions).
2.6.1 Invoke
invoke(block_or_function, arguments)
invoke calls a block, lambda, or pointer to a function (block_or_function) with the provided list of arguments.
(see Functions, Blocks, Lambdas).
2.6.2 Misc
assert(x, str)
assert causes an application-defined assert if the x argument is false. assert can and probably will be
removed from release builds. That’s why it will not compile if the x argument has side effects (for example,
calling a function with side effects).
verify(x, str)
verify causes an application-defined assert if the x argument is false. The verify check can be removed
from release builds, but execution of the x argument stays. That’s why verify, unlike assert, can have side
effects in evaluating x.
static_assert(x, str)
static_assert causes the compiler to stop compilation if the x argument is false. That’s why x has to be a
compile-time known constant. ``static_assert``s are removed from compiled programs.
concept_assert(x, str)
concept_assert is similar to static_assert, but errors will be reported one level above the assert.
That way applications can report contract errors.
debug(x, str)
debug prints string str and the value of x (like print). However, debug also returns the value of x, which makes
it suitable for debugging expressions:
2.7 Clone
Clone is designed to create a deep copy of the data. Cloning is invoked via the clone operator :=:
a := b
Cloning can be also invoked via the clone initializer in a variable declaration:
var x := y
(see clone_to_move).
struct Foo
a : int
Cloning is typically allowed between regular and temporary types (see Temporary types).
POD types are copied instead of cloned:
2.7. Clone 27
daScript Reference Manual, Release 0.2 beta
a = b
c = d
Handled types provide their own clone functionality via canClone, simulateClone, and appropriate
das_clone C++ infrastructure (see Handles).
For static arrays, the clone_dim generic is called, and for dynamic arrays, the clone generic is called. Those in
turn clone each of the array elements:
struct Foo
a : array<int>
b : int
var a, b : array<Foo>
b := a
var c, d : Foo[10]
c := d
For tables, the clone generic is called, which in turn clones its values:
var a, b : table<string;Foo>
b := a
clear(a)
for k,v in keys(b),values(b)
a[k] := v
For structures, the default clone function is generated, in which each element is cloned:
struct Foo
a : array<int>
b : int
dest._0 = src._0
dest._1 := src._1
dest._2 = src._2
var a, b : variant<i:int;a:array<int>;s:string>
b := a
if src is i
set_variant_index(dest,0)
dest.i = src.i
elif src is a
set_variant_index(dest,1)
dest.a := src.a
elif src is s
set_variant_index(dest,2)
dest.s = src.s
Note that for non-cloneable types, daScript will not promote := initialize into clone_to_move.
2.8 Unsafe
The unsafe keyword denotes unsafe contents, which is required for operations, but could potentially crash the
application:
unsafe
let px = addr(x)
let px = unsafe(addr(x))
Unsafe is followed by a block which can include those operations. Nested unsafe sections are allowed. Unsafe is not
inherited in lambda, generator, or local functions; it is, however, inherited in local blocks.
Individual expressions can cause a CompilationError::unsafe error, unless they are part of the unsafe section. Addi-
tionally, macros can explicitly set the ExprGenFlags::alwaysSafe flag.
2.8. Unsafe 29
daScript Reference Manual, Release 0.2 beta
unsafe
let a : int
let pa = addr(a)
return pa // accessing *pa can potentially corrupt
˓→stack
Lambdas or generators require unsafe sections for the implicit capture by move or by reference:
var a : array<int>
unsafe
var counter <- @ <| (extra:int) : int
return a[0] + extra // a is implicitly moved
unsafe
return reinterpret<void?> 13 // reinterpret can create unsafe pointers
unsafe
var p = new Foo()
return p[13] // accessing out of bounds pointer can
˓→potentially corrupt memory
A safe index is unsafe when not followed by the null coalescing operator:
var a = {{ 13 => 12 }}
unsafe
var t = a?[13] ?? 1234 // safe
return a?[13] // unsafe; safe index is a form of 'addr'
˓→operation
Variant ?as on local variables is unsafe when not followed by the null coalescing operator:
unsafe
return a ?as Bar // safe as is a form of 'addr' operation
Variant .?field is unsafe when not followed by the null coalescing operator:
unsafe
return a?.Bar // safe navigation of a variant is a form
˓→of 'addr' operation
unsafe
return a.Bar // this is potentially a reinterpret cast
Certain functions and operators are inherently unsafe or marked unsafe via the [unsafe_operation] annotation:
unsafe
var a : int?
a += 13 // pointer arithmetic can create invalid
˓→pointers
Moving from a smart pointer value requires unsafe, unless that value is the ‘new’ operator:
unsafe
var a <- new TestObjectSmart() // safe, its explicitly new
var b <- someSmartFunction() // unsafe since lifetime is not obvious
b <- a // safe, values are not lost
unsafe
var g = Goo() // potential lifetime issues
2.9 Table
There are several relevant builtin functions: clear, key_exists, find, and erase. For safety, find doesn’t
return anything. Instead, it works with block as last argument. It can be used with the rbpipe operator:
If it was not done this way, find would have to return a pointer to its value, which would continue to point ‘some-
where’ even if data was deleted. Consider this hypothetical find in the following example:
2.9. Table 31
daScript Reference Manual, Release 0.2 beta
So, if you just want to check for the existence of a key in the table, use key_exists(table, key).
Tables (as well as arrays, structs, and handled types) are passed to functions by reference only.
Tables cannot be assigned, only cloned or moved.
Table keys can be not only strings, but any other ‘workhorse’ type as well.
Tables can be constructed inline:
2.10 Array
An array is a sequence of values indexed by an integer number from 0 to the size of the array minus 1. An array’s
elements can be obtained by their index.
var b: array<int>
push(b,1)
assert(b[0] == 1)
There are static arrays (of fixed size, allocated on the stack), and dynamic arrays (size is dynamic, allocated on the
heap):
Dynamic sub-arrays can be created out of any array type via range indexing:
When array elements can’t be copied, use push_clone to insert a clone of a value, or emplace to move it in.
resize can potentially create new array elements. Those elements are initialized with 0.
reserve is there for performance reasons. Generally, array capacity doubles, if exceeded. reserve allows you to
specify the exact known capacity and significantly reduce the overhead of multiple push operations.
It’s possible to iterate over an array via a regular for loop:
2.10. Array 33
daScript Reference Manual, Release 0.2 beta
The reason both are unsafe operations is that they do not capture the array.
Search functions are available for both static and dynamic arrays:
2.11 Function
Functions pointers are first class values, like integers or strings, and can be stored in table slots, local variables, arrays,
and passed as function parameters. Functions themselves are declarations (much like in C++).
def foo
print("foo")
//same as above
def foo()
print("foo")
daScript can always infer a function’s return type. Returning different types is a compilation error:
def foo(a:bool)
if a
return 1
else
return 2.0 // error, expecting int
Publicity
If not specified, functions inherit module publicity (i.e. in public modules functions are public, and in private modules
functions are private).
Function calls
You can call a function by using its name and passing in all its arguments (with the possible omission of the default
arguments):
def bar
foo(1, 2) // a = 1, b = 2
You can also call a function by using its name and passing all aits rguments with explicit names (with the possible
omission of the default arguments):
def bar
foo([a = 1, b = 2]) // same as foo(1, 2)
def bar
foo([b = 1, a = 2]) // error, out of order
Named argument calls increase the readability of callee code and ensure correctness in refactorings of the existing
functions. They also allow default values for arguments other than the last ones:
def bar
foo([b = 2]) // same as foo(13, 2)
Function pointer
def twice(a:int)
return a + a
let fn = @@twice
2.11. Function 35
daScript Reference Manual, Release 0.2 beta
When multiple functions have the same name, a pointer can be obtained by explicitly specifying signature:
def twice(a:int)
return a + a
let t = invoke(fn, 1) // t = 2
Nameless functions
Pointers to nameless functions can be created with a syntax similar to that of lambdas or blocks (see Blocks):
var count = 1
let fn <- @@ <| ( a : int )
return a + count // compilation error, can't locate variable count
Generic functions
Generic functions are similar to C++ templated functions. daScript will instantiate them during the infer pass of
compilation:
def twice(a)
return a + a
Generic functions allow code similar to dynamically-typed languages like Python or Lua, while still enjoying the
performance and robustness of strong, static typing.
Generic function addresses cannot be obtained.
Unspecified types can also be written via auto notation:
Generic functions can specialize generic type aliases, and use them as part of the declaration:
def twice(a:auto(TT)) : TT
return a + a
In the example above, alias TT is used to enforce the return type contract.
Type aliases can be used before the corresponding auto:
In the example above, TT is inferred from the type of the passed array a, and expected as a first argument base. The
return type is inferred from the type of s, which is also TT.
Function overloading
Declaring functions with the same exact argument list is compilation time error.
Functions can be partially specialized:
daScript uses the following rules for matching partially specialized functions:
1. Non-auto is more specialized than auto.
2. If both are non-auto, the one without a cast is more specialized.
3. Ones with arrays are more specialized than ones without. If both have an array, the one with the actual value is
more specialized than the one without.
4. Ones with a base type of autoalias are less specialized. If both are autoalias, it is assumed that they have the
same level of specialization.
5. For pointers and arrays, the subtypes are compared.
6. For tables, tuples and variants, subtypes are compared, and all must be the same or equally specialized.
2.11. Function 37
daScript Reference Manual, Release 0.2 beta
7. For functions, blocks, or lambdas, subtypes and return types are compared, and all must be the same or equally
specialized.
When matching functions, daScript picks the ones which are most specialized and sorts by substitute distance. Sub-
stitute distance is increased by 1 for each argument if a cast is required for the LSP (Liskov substitution principle). At
the end, the function with the least distance is picked. If more than one function is left for picking, a compilation error
is reported.
Function specialization can be limited by contracts (contract macros):
def foo ( a : Foo explicit ) // will accept Foo, but not any subtype of Foo
Default Parameters
When the function test is invoked and the parameters c or d are not specified, the compiler will generate a call
with default value to the unspecified parameter. A default parameter can be any valid compile-time const daScript
expression. The expression is evaluated at compile-time.
It is valid to declare default values for arguments other than the last one:
Calling such functions with default arguments requires a named arguments call:
There are no methods or function members of structs in daScript. However, code can be easily written “OOP style”
by using the right pipe operator |>:
struct Foo
x, y: int = 0
(see Structs).
Tail recursion is a method for partially transforming recursion in a program into iteration: it applies when the recursive
calls in a function are the last executed statements in that function (just before the return).
Currently, daScript doesn’t support tail recursion. It is implied that a daScript function always returns.
2.12 Modules
Modules provide infrastructure for code reuse, as well as mechanism to expose C++ functionality to daScript. A
module is a collection of types, constants, and functions. Modules can be native to daScript, as well as built-in.
To request a module, use the require keyword:
require math
require ast public
require daslib/ast_boost
The public modifier indicates that included model is visible to everything including current module.
Module names may contain / and . symbols. The project is responsible for resolving module names into file names
(see Project).
If not specified, the module name defaults to that of the file name.
Modules can be private or public:
2.12. Modules 39
daScript Reference Manual, Release 0.2 beta
Default publicity of the functions, structures, or enumerations are that of the module (i.e. if the module is public and
a function’s publicity is not specified, that function is public).
Builtin modules are the way to expose C++ functionality to daScript (see Builtin modules).
Shared modules are modules that are shared between compilation of multiple contexts. Typically, modules are com-
piled anew for each context, but when the ‘shared’ keyword is specified, the module gets promoted to a builtin module:
That way only one instance of the module is created per compilation environment. Macros in shared modules can’t
expect the module to be unique, since sharing of the modules can be disabled via the code of policies.
When calling a function, the name of the module can be specified explicitly or implicitly:
If the function does not exist in that module, a compilation error will occur. If the function is private or not directly
visible, a compilation error will occur. If multiple functions match implicit function, compilation error will occur.
Module names _ and __ are reserved to specify the current module and the current module only, respectively. Its
particularly important for generic functions, which are always instanced as private functions in the current module:
module b
[generic]
def from_b_get_fun_4()
return _::fun_4() // call `fun_4', as if it was implicitly called from b
[generic]
def from_b_get_fun_5()
return __::fun_5() // always b::fun_5
2.13 Block
Blocks are nameless functions which captures the local context by reference. Blocks offer significant performance
advantages over lambdas (see Lambda).
The block type can be declared with a function-like syntax:
block_type ::= block { optional_block_type }
optional_block_type ::= < { optional_block_arguments } { : return_type } >
optional_block_arguments := ( block_argument_list )
block_argument_list := argument_name : type | block_argument_list ; argument_name :
˓→type
Blocks capture the current stack, so blocks can be passed, but never returned. Block variables can only be passed as
arguments. Global or local block variables are prohibited; returning the block type is also prohibited:
def goo ( b : block )
...
There is a simplified syntax for blocks that only contain a return expression:
res = radd(v1, $(var a:int&) : int => a++ ) // equivalent to example above
If a block is sufficiently specified in the generic or function, block types will be automatically inferred:
res = radd(v1, $(a) => a++ ) // equivalent to example above
2.13. Block 41
daScript Reference Manual, Release 0.2 beta
while true
take_any() <|
break // 30801, captured block can't break outside the block
def queryOne(dt:float=1.0f)
testProfile::queryEs() <| $ [es] (var pos:float3&;vel:float3 const) // [es] is
˓→annotation
pos += vel * dt
2.14 Lambda
Lambdas are nameless functions which capture the local context by clone, copy, or reference. Lambdas are slower
than blocks, but allow for more flexibility in lifetime and capture modes (see Blocks).
The lambda type can be declared with a function-like syntax:
Lambdas can be local or global variables, and can be passed as an argument by reference. Lambdas can be moved, but
can’t be copied or cloned:
var CNT = 0
let counter <- @ <| (extra:int) : int
(continues on next page)
There are a lot of similarities between lambda and block declarations. The main difference is that blocks are specified
with $ symbol, while lambdas are specified with @ symbol. Lambdas can also be declared via inline syntax. There is
a similar simplified syntax for the lambdas containing return expression only. If a lambda is sufficiently specified in
the generic or function, its types will be automatically inferred (see Blocks).
2.14.1 Capture
Unlike blocks, lambdas can specify their capture types explicitly. There are several available types of capture:
• by copy
• by move
• by clone
• by reference
Capturing by reference requires unsafe.
By default, capture by copy will be generated. If copy is not available, unsafe is required for the default capture by
move:
Lambdas can be deleted, which cause finalizers to be called on all captured data (see Finalizers):
delete lam
Lambdas can specify a custom finalizer which is invoked before the default finalizer:
var CNT = 0
var counter <- @ <| (extra:int) : int
return CNT++ + extra
finally
print("CNT = {CNT}\n")
var x = invoke(counter,13)
delete counter // this is when the finalizer is called
2.14. Lambda 43
daScript Reference Manual, Release 0.2 beta
2.14.2 Iterators
Lambdas are the main building blocks for implementing custom iterators (see Iterators).
Lambdas can be converted to iterators via the each or each_ref functions:
var count = 0
let lam <- @ <| (var a:int &) : bool
if count < 10
a = count++
return true
else
return false
for x,tx in each(lam),range(0,10)
assert(x==tx)
Lambdas are implemented by creating a nameless structure for the capture, as well as a function for the body of the
lambda.
Let’s review an example with a singled captured variable:
var CNT = 0
let counter <- @ <| (extra:int) : int
return CNT++ + extra
struct _lambda_thismodule_7_8_1
__lambda : function<(__this:_lambda_thismodule_7_8_1;extra:int const):int> = @@_
˓→lambda_thismodule_7_8_1`function
CNT : int
Body function:
with __this
return CNT++ + extra
Finalizer function:
delete *this
delete __this
The C++ Lambda class contains single void pointer for the capture data:
struct Lambda {
...
char * capture;
...
};
The rational behind passing lambda by reference is that when delete is called
1. the finalizer is invoked for the capture data
2. the capture is replaced via null
The lack of a copy or move ensures there are not multiple pointers to a single instance of the captured data floating
around.
2.15 Struct
daScript uses a structure mechanism similar to languages like C/C++, Java, C#, etc. However, there are some important
difference. Structures are first class objects like integers or strings and can be stored in table slots, other structures,
local variables, arrays, tuples, variants, etc., and passed as function parameters.
struct Foo
x, y: int
xf: float
If not specified, structures inherit module publicity (i.e. in public modules structures are public, and in private modules
structures are private).
Structure instances are created through a ‘new expression’ or a variable declaration statement:
There are intentionally no member functions. There are only data members, since it is a data type itself. Structures can
handle members with a function type as data (meaning it’s a function pointer that can be changed during execution).
There are initializers that simplify writing complex structure initialization. Basically, a function with same name as
2.15. Struct 45
daScript Reference Manual, Release 0.2 beta
the structure itself works as an initializer. The compiler will generates a ‘default’ initializer if there are any members
with an initializer:
struct Foo
x: int = 1
y: int = 2
Structure fields are initialized to zero by default, regardless of ‘initializers’ for members, unless you specifically call
the initializer:
struct Foo
x = 1 // inferred as int
y = 2.0 // inferred as float
Explicit structure initialization during creation leaves all uninitialized members zeroed:
The “Clone initializer” is useful pattern for creating a clone of an existing structure when both structures are on the
heap:
var self := *p
return <- self
...
let a = new [[Foo x=1, y=2.]] // create new instance of Foo on the heap,
˓→initialize it
daScript doesn’t have embedded structure member functions, virtual (that can be overridden in inherited structures)
or non-virtual. Those features are implemented for classes. For ease of Objected Oriented Programming, non-virtual
member functions can be easily emulated with the pipe operator |>:
struct Foo
x, y: int = 0
Since function pointers are a thing, one can emulate ‘virtual’ functions by storing function pointers as members:
struct Foo
x, y: int = 0
set = @@setXY
This makes the difference between virtual and non-virtual calls in the OOP paradigm explicit. In fact, daScript classes
implement virtual functions in exactly this manner.
2.15.3 Inheritance
daScript’s structures support single inheritance by adding a ‘ : ‘, followed by the parent structure’s name in the structure
declaration. The syntax for a derived struct is the following:
When a derived structure is declared, daScript first copies all base’s members to the new structure and then proceeds
with evaluating the rest of the declaration.
A derived structure has all members of its base structure. It is just syntactic sugar for copying all the members manually
first.
2.15. Struct 47
daScript Reference Manual, Release 0.2 beta
2.15.4 Alignment
[cpp_layout (pod=false)]
struct CppS1
vtable : void? // we are simulating C++ class
b : int64 = 2l
c : int = 3
[cpp_layout (pod=false)]
struct CppS2 : CppS1 // d will be aligned on the class bounds
d : int = 4
2.15.5 OOP
There is sufficient amount of infrastructure to support basic OOP on top of the structures. However, it is already
available in form of classes with some fixed memory overhead (see Classes).
It’s possible to override the method of the base class with override syntax. Here an example:
struct Foo
x, y: int = 0
set = @@Foo_setXY
It is safe to use the cast keyword to cast a derived structure instance into its parent type:
struct Foo
x: int
struct Foo2:Foo
y: int
def setY(var foo: Foo; y: int) // Warning! Can make awful things to your app if its
˓→not really Foo2
unsafe
(upcast<Foo2> foo).y = y
As the example above is very dangerous, and in order to make it safer, you can modify it to following:
struct Foo
x: int
typeTag: uint = hash("Foo")
struct Foo2:Foo
y: int
override typeTag: uint = hash("Foo2")
def setY(var foo: Foo; y: int) // this won't do anything really bad, but will panic
˓→on wrong reference
unsafe
if foo.typeTag == hash("Foo2")
(upcast<Foo2> foo).y = y
print("Foo2 type references was passed\n")
else
assert(false, "Not Foo2 type references was passed\n")
2.16 Tuple
Two tuple declarations are the same if they have the same number of types, and their respective types are the same:
Tuple elements can be accessed via nameless fields, i.e. _ followed by the 0 base field index:
a._0 = 1
a._1 = 2.0
Named tuple elements can be accessed by name as well as via nameless field:
b.i = 1 // same as _0
b.f = 2.0 // same as _1
b._1 = 2.0 // _1 is also available
2.16. Tuple 49
daScript Reference Manual, Release 0.2 beta
2.17 Variant
Variants are nameless types which provide support for values that can be one of a number of named cases, possibly
each with different values and types:
var t : variant<i_value:uint;f_value:float>
typedef
U_F = variant<i_value:uint;f_value:float> // exactly the same as the declaration
˓→above
Any two variants are the same type if they have the same named cases of the same types in the same order.
Variants hold the index of the current case, as well as the value for the current case only.
The current case selection can be checked via the is operator, and accessed via the as operator:
assert(t is i_value)
assert(t as i_value == 0x3f800000)
The entire variant selection can be modified by copying the properly constructed variant of a different case:
t = [[U_F i_value = 0x40000000]] // now case is i_value
t = [[U_F f_value = 1.0]] // now case is f_value
Cases can also be accessed in an unsafe manner without checking the type:
unsafe
t.i_value = 0x3f800000
return t.f_value // will return memory, occupied by f_value -
˓→i.e. 1.0f
The index value for a specific case can be determine via the variant_index and safe_variant_index type
traits. safe_variant_index will return -1 for invalid indices and types, whereas variant_index will report
a compilation error:
assert(typeinfo(variant_index<i_value> t)==0)
assert(typeinfo(variant_index<f_value> t)==1)
assert(typeinfo(variant_index<unknown_value> t)==-1) // compilation error
assert(typeinfo(safe_variant_index<i_value> t)==0)
assert(typeinfo(safe_variant_index<f_value> t)==1)
assert(typeinfo(safe_variant_index<unknown_value> t)==-1)
Current case selection can be modified with the unsafe operation safe_variant_index:
unsafe
set_variant_index(t, typeinfo(variant_index<f_value> t))
Variants contain the ‘index’ of the current case, followed by a union of individual cases, similar to the following C++
layout:
struct MyVariantName {
int32_t __variant_index;
union {
type0 case0;
type1 case1;
...
};
};
2.18 Class
In daScript, classes are an extension of structures designed to provide OOP capabilities. Classes provides single parent
inheritance, abstract and virtual methods, initializers, and finalizers.
The basic class declaration is similar to that of a structure, but with the class keyword:
class Foo
x, y : int = 0
def Foo // custom initializer
Foo`set(self,1,1)
def set(X,Y:int) // inline method
x = X
y = Y
The initializer is a function with a name matching that of a class. Classes can have multiple initializer with different
arguments:
class Foo
...
def Foo(T:int) // custom initializer
self->set(T,T)
(continues on next page)
2.18. Class 51
daScript Reference Manual, Release 0.2 beta
class Foo
...
def finalize // custom finalizer
delFoo ++
class FooAbstract
def abstract set(X,Y:int) : void // inline method
Abstract functions need to be fully qualified, including their return type. Class member functions are inferred in the
same manner as regular functions.
Sealed functions cannot be overridden. The sealed keyword is used to prevent overriding:
xyz = X + Y
Sealed classes can not be inherited from. The sealed keyword is used to prevent inheritance:
unsafe
var f = Foo() // unsafe
f->set(1,2)
Foo`set(*f,1,2)
Class initializers are generated by adding a local self variable with construct syntax. The body of the method is
prefixed via a with self expression. The final expression is a return <- self:
Class methods and finalizers are generated by providing the extra argument self. The body of the method is prefixed
with a with self expression:
invoke(f3d.set,cast<Foo> f3d,1,2)
Every base class gets an __rtti pointer, and a __finalize function pointer. Additionally, a function pointer is
added for each member function:
class Foo
__rtti : void? = typeinfo(rtti_classinfo type<Foo>)
__finalize : function<(self:Foo):void> = @@_::Foo'__finalize
x : int = 0
y : int = 0
set : function<(self:Foo;X:int const;Y:int const):void> = @@_::Foo`set
__rtti contains rtti::TypeInfo for the specific class instance. There is helper function in the rtti module to access
class_info safely:
The finalize pointer is invoked when the finalizer is called for the class pointer. That way, when delete is called
on the base class pointer, the correct version of the derived finalizer is called.
2.18. Class 53
daScript Reference Manual, Release 0.2 beta
daScript allows you to bind constant values to a global variable identifier. Whenever possible, all constant global
variables will be evaluated at compile time. There are also enumerations, which are strongly typed constant collections
similar to enum classes in C++.
2.19.1 Constant
Constants bind a specific value to an identifier. Constants are exactly global variables. Their value cannot be changed.
Constants are declared with the following syntax:
let
foobar = 100
let
floatbar = 1.2
let
stringbar = "I'm a constant string"
let blah = "I'm string constant which is declared on the same line as variable"
Constants are always globally scoped from the moment they are declared. Any subsequential code can reference them.
You can not change such global variables.
Constants can be shared:
Shared constants point to the same memory in different instances of Context. They are initialized once during the first
context initialization.
var
foobar = 100
var barfoo = 100
Their usage can be switched on and off on a per-project basis via CodeOfPolicies.
Local static variables can be declared via the static_let macro:
require daslib/static_let
def foo
static_let <|
var bar = 13
bar = 14
Variable bar in the example above is effectively a global variable. However, it’s only visible inside the scope of the
corresponding static_let macro.
Global variables can be private or public
If not specified, structures inherit module publicity (i.e. in public modules global variables are public, and in private
modules global variables are private).
2.19.3 Enumeration
An enumeration binds a specific value to a name. Enumerations are also evaluated at compile time and their value
cannot be changed.
An enum declaration introduces a new enumeration to the program. Enumeration values can only be compile time
constant expressions. It is not required to assign specific value to enum:
enum Numbers
zero // will be 0
one // will be 1
two // will be 2
ten = 9+1 // will be 10, since its explicitly specified
If not specified, enumeration inherit module publicity (i.e. in public modules enumerations are public, and in private
modules enumerations are private).
An enum name itself is a strong type, and all enum values are of this type. An enum value can be addressed as ‘enum
name’ followed by exact enumeration
Enumerations can specify one of the following storage types: int, int8, int16, uint, uint8, or uint16:
2.20 Bitfield
Bitfields are a nameless types which represent a collection of up to 32-bit flags in a single integer:
bitfield bits123
one
two
three
typedef
bits123 = bitfield<one; two; three> // exactly the same as the declaration above
Any two bitfields are the same type and represent 32-bit integer:
assert(t==bits123 three)
Bitfields can be constructed via an integer value. Limited binary logical operators are available:
2.21 Comprehension
Comprehensions are concise notation constructs designed to allow sequences to be built with other sequences.
The syntax is inspired by that of a for loop:
Comprehension produces either an iterator or a dynamic array, depending on the style of brackets:
var a3 <- [{for x in range(0,10); x; where (x & 1) == 1}] // only odd numbers
Just like a for loop, comprehension can iterate over multiple sources:
Regular lambda capturing rules are applied for iterator comprehensions (see Lambdas).
Internally array comprehension produces an invoke of a local block and a for loop; whereas iterator comprehension
produces a generator (lambda). Array comprehensions are typically faster, but iterator comprehensions have less of a
memory footprint.
2.22 Iterator
Iterators are objects which can traverse over a sequence without knowing the details of the sequence’s implementation.
The Iterator type is defined as follows:
unsafe
var it <- each ( [[int 1;2;3;4]] )
For the reference iterator, the for loop will provide a reference variable:
Iterators can be created from lambdas (see Lambda) or generators (see Generator).
Calling delete on an iterator will make it sequence out and free its memory:
2.22. Iterator 57
daScript Reference Manual, Release 0.2 beta
Table keys and values iterators can be obtained via the keys and values functions:
It is possible to iterate over each character of the string via the each function:
unsafe
for ch in each("hello,world!") // string iterator is iterator<int>
print("ch = {ch}\n")
It is possible to iterate over each element of an enumeration via the each_enum function:
enum Numbers
one
two
ten = 10
unsafe
var it <- each ( [[int 1;2;3;4]] )
for x in it
print("x = {x}\n")
verify(empty(it)) // iterator is sequenced out
var x : int
while next(it,x) // this is semantically equivalent to the `for x in it`
print("x = {x}\n")
_builtin_iterator_iterate is one function to rule them all. It acts like all 3 functions above. On a non-
empty iterator it starts with ‘first’, then proceeds to call next until the sequence is exhausted. Once the iterator is
sequenced out, it calls close:
It is important to notice that builtin iteration functions accept pointers instead of references.
2.22. Iterator 59
daScript Reference Manual, Release 0.2 beta
2.23 Generator
Generators allow you to declare a lambda that behaves like an iterator. For all intents and purposes, a generator is a
lambda passed to an each or each_ref function.
Generator syntax is similar to lambda syntax:
Generators can output ref types. They can have a capture section:
Generators can have a finally expression on its blocks, with the exception of the if-then-else blocks:
__yield : int
_loop_at_8 : bool
x : int // captured constant
_pvar_0_at_8 : void?
_source_0_at_8 : iterator<int>
goto __this.__yield
label 0:
__this._loop_at_8 = true
__this._source_0_at_8 <- __::builtin`each(range(0,10))
memzero(__this.x)
__this._pvar_0_at_8 = reinterpret<void?> addr(__this.x)
__this._loop_at_8 &&= _builtin_iterator_first(__this._source_0_at_8,__this._pvar_
˓→0_at_8,__context__)
2.23. Generator 61
daScript Reference Manual, Release 0.2 beta
Control flow statements are replaced with the label + goto equivalents. Generators always start with goto
__this.yield. This effectively produces a finite state machine, with the yield variable holding current state
index.
The yield expression is converted into a copy result and return value pair. A label is created to specify where to go
to next time, after the yield:
2.24 Finalizer
Finalizers are special functions which are called in exactly two cases:
delete is called explicitly on a data type:
Custom finalizers can be defined for any type by overriding the finalize function. Generic custom finalizers are
also allowed:
struct Foo
a : int
Static arrays call finalize_dim generically, which finalizes all its values:
var f : Foo[5]
delete f
Dynamic arrays call finalize generically, which finalizes all its values:
var f : array<Foo>
delete f
Tables call finalize generically, which finalizes all its values, but not its keys:
var f : table<string;Foo>
delete f
2.24. Finalizer 63
daScript Reference Manual, Release 0.2 beta
Custom finalizers are generated for structures. Fields annotated as [[do_not_delete]] are ignored. memzero clears
structure memory at the end:
struct Goo
a : Foo
[[do_not_delete]] b : array<int>
Variants behave similarly to tuples. Only the currently active variant is finalized:
Lambdas and generators have their capture structure finalized. Lambdas can have custom finalizers defined as well
(see Lambdas).
Classes can define custom finalizers inside the class body (see Classes).
Instead of formatting strings with variant arguments count function (like printf), daScript provides String builder
functionality out-of-the-box. It is both more readable, more compact and more robust than printf-like syntax. All
strings in daScript can be either string literals, or built strings. Both are written with “”, but string builder strings also
contain any expression in curly brackets ‘{}’:
In the example above, str2 will actually be compile-time defined, as the expression in {} is compile-time computable.
But generally, they can be run-time compiled as well. Expressions in {} can be of any type, including handled extern
type, provided that said type implements DataWalker. All PODs in daScript do have DataWalker ‘to string’
implementation.
In order to make a string with {} inside, one has to escape curly brackets with ‘':
daScript allows ommision of types in statements, functions, and function declaration, making writing in it similar to
dynamically typed languages, such as Python or Lua. Said functions are instantiated for specific types of arguments
on the first call.
There are also ways to inspect the types of the provided arguments, in order to change the behaviour of function, or to
provide reasonable meaningful errors during the compilation phase. Most of these ways are achieved with s
Unlike C++ with its SFINAE, you can use common conditionals (if) in order to change the instance of the function
depending on the type info of its arguments. Consider the following example:
This function sets someField in the provided argument if it is a struct with a someField member.
We can do even more. For example:
obj.someField = val
This function sets someField in the provided argument if it is a struct with a someField member, and only if
someField is of the same type as val!
2.26.1 typeinfo
Most type reflection mechanisms are implemented with the typeinfo operator. There are:
• typeinfo(typename object) // returns typename of object
• typeinfo(fulltypename object) // returns full typename of object, with contracts (like !const, or !&)
• typeinfo(sizeof object) // returns sizeof
• typeinfo(is_pod object) // returns true if object is POD type
• typeinfo(is_raw object) // returns true if object is raw data, i.e., can be copied with memcpy
• typeinfo(is_struct object) // returns true if object is struct
• typeinfo(has_field<name_of_field> object) // returns true if object is struct with field
name_of_field
• typeinfo(is_ref object) // returns true if object is reference to something
• typeinfo(is_ref_type object) // returns true if object is of reference type (such as array, table,
das_string or other handled reference types)
• typeinfo(is_const object) // returns true if object is of const type (i.e., can’t be modified)
• typeinfo(is_pointer object) // returns true if object is of pointer type, i.e., int?
All typeinfo can work with types, not objects, with the type keyword:
Instead of ommitting the type name in a generic, it is possible to use an explicit auto type or auto(name) to type
it:
or
def fn(a)
return a
This is very helpful if the function accepts numerous arguments, and some of them have to be of the same type:
def set0(a, b; index: int) // a is only supposed to be of array type, of same type as
˓→b
return a[index] = b
If you call this function with an array of floats and an int, you would get a not-so-obvious compiler error message:
return a[index] = b
2.27 Macros
In daScript, macros are the machinery that allow direct manipulation of the syntax tree.
Macros are exposed via the daslib/ast module and daslib/ast_boost helper module.
Macros are evaluated at compilation time during different compilation passes. Macros assigned to a specific module
are evaluated as part of the module every time that module is included.
The daScript compiler performs compilation passes in the following order for each module (see Modules):
1. Parser transforms das program to AST
1. If there are any parsing errors, compilation stops
2. apply is called for every function or structure
1. If there are any errors, compilation stops
3. Infer pass repeats itself until no new transformations are reported
1. Built-in infer pass happens
1. transform macros are called for every function or expression
2. Macro passes happen
4. If there are still any errors left, compilation stops
5. finish is called for all functions and structure macros
6. Lint pass happens
2.27. Macros 67
daScript Reference Manual, Release 0.2 beta
The [_macro] annotation is used to specify functions that should be evaluated at compilation time . Consider the
following example from daslib/ast_boost:
[_macro,private]
def setup
if is_compiling_macros_in_module("ast_boost")
add_new_function_annotation("macro", new MacroMacro())
The setup function is evaluated after the compilation of each module, which includes ast_boost. The
is_compiling_macros_in_module function returns true if the currently compiled module name matches the
argument. In this particular example, the function annotation macro would only be added once: when the module
ast_boost is compiled.
Macros are invoked in the following fashion:
1. Class is derived from the appropriate base macro class
2. Adapter is created
3. Adapter is registered with the module
For example, this is how this lifetime cycle is implemented for the reader macro:
2.27.3 AstFunctionAnnotation
The AstFunctionAnnotation macro allows you to manipulate calls to specific functions as well as their function
bodies. Annotations can be added to regular or generic functions.
add_new_function_annotation adds a function annotation to a module. There is additionally the
[function_macro] annotation which accomplishes the same thing.
AstFunctionAnnotation allows several different manipulations:
class AstFunctionAnnotation
def abstract transform ( var call : smart_ptr<ExprCallFunc>; var errors : das_
˓→string ) : ExpressionPtr
˓→bool
transform lets you change calls to the function and is applied at the infer pass. Transform is the best way to replace
or modify function calls with other semantics.
verifyCall is called durng the lint phase on each call to the function and is used to check if the call is valid.
apply is applied to the function itself before the infer pass. Apply is typically where global function body modifica-
tions or instancing occurs.
finish is applied to the function itself after the infer pass. It’s only called on non-generic functions or instances of
the generic functions. finish is typically used to register functions, notify C++ code, etc. After this, the function is
fully defined and inferred, and can no longer be modified.
patch is called after the infer pass. If patch sets astChanged to true, the infer pass will be repeated.
fixup is called after the infer pass. It’s used to fixup the function’s body.
lint is called during the lint phase on the function itself and is used to verify that the function is valid.
complete is called during the simulate portion of context creation. At this point Context is available.
isSpecialized must return true if the particular function matching is governed by contracts. In that case,
isCompatible is called, and the result taken into account.
isCompatible returns true if a specialized function is compatible with the given arguments. If a function is not
compatible, the errors field must be specified.
appendToMangledName is called to append a mangled name to the function. That way multiple functions with the
same type signature can exist and be differentiated between.
2.27. Macros 69
daScript Reference Manual, Release 0.2 beta
Lets review the following example from ast_boost of how the macro annotation is implemented:
During the apply pass the function body is appended with the if is_compiling_macros() closure. Addi-
tionally, the init flag is set, which is equivalent to a _macro annotation. Functions annotated with [macro] are
evaluated during module compilation.
2.27.4 AstBlockAnnotation
class AstBlockAnnotation
def abstract apply ( var blk:smart_ptr<ExprBlock>; var group:ModuleGroup;
˓→args:AnnotationArgumentList; var errors : das_string ) : bool
2.27.5 AstStructureAnnotation
The AstStructureAnnotation macro lets you manipulate structure or class definitions via annotation:
class AstStructureAnnotation
def abstract apply ( var st:StructurePtr; var group:ModuleGroup;
˓→args:AnnotationArgumentList; var errors : das_string ) : bool
class AstStructureAnnotation
def abstract apply ( var st:StructurePtr; var group:ModuleGroup;
˓→args:AnnotationArgumentList; var errors : das_string ) : bool
apply is invoked before the infer pass. It is the best time to modify the structure, generate some code, etc.
finish is invoked after the successful infer pass. Its typically used to register structures, perform RTTI operations,
etc. After this, the structure is fully inferred and defined and can no longer be modified afterwards.
patch is invoked after the infer pass. If patch sets astChanged to true, the infer pass will be repeated.
complete is invoked during the simulate portion of context creation. At this point Context is available.
An example of such annotation is SetupAnyAnnotation from daslib/ast_boost.
2.27.6 AstEnumerationAnnotation
2.27.7 AstVariantMacro
if isExpression(expr.value._type)
var vdr <- new [[ExprField() at=expr.at, name:="__rtti", value <- clone_
˓→expression(expr.value)]]
2.27. Macros 71
daScript Reference Manual, Release 0.2 beta
...
Here, the macro takes advantage of the ExprIsVariant syntax. It replaces the expr is TYPENAME expression
with an expr.__rtti = "TYPENAME" expression. The isExpression function ensures that expr is from the
ast::Expr* family, i.e. part of the daScript syntax tree.
2.27.8 AstReaderMacro
Reader macros are invoked via the % READER_MACRO_NAME ~ character_sequence syntax. The accept
function notifies the correct terminator of the character sequence:
var x = %arr~\{\}\w\x\y\n%% // invoking reader macro arr, %% is a terminator
append(expr.sequence,ch)
if ends_with(expr.sequence,"%%")
let len = length(expr.sequence)
resize(expr.sequence,len-2)
return false
else
return true
def override visit ( prog:ProgramPtr; mod:Module?; expr:smart_ptr<ExprReader> ) :
˓→ExpressionPtr
The accept function macro collects symbols in the sequence. Once the sequence ends with the terminator sequence
%%, accept returns false to indicate the end of the sequence.
In visit, the collected sequence is converted into a make array [[int ch1; ch2; ..]] expression.
More complex examples include the JsonReader macro in daslib/json_boost or RegexReader in daslib/regex_boost.
2.27.9 AstCallMacro
AstCallMacro operates on expressions which have function call syntax or something similar. It occurs during the
infer pass.
add_new_call_macro adds a call macro to a module. The [call_macro] annotation automates the same
thing:
class AstCallMacro
def abstract preVisit ( prog:ProgramPtr; mod:Module?; expr:smart_ptr
˓→<ExprCallMacro> ) : void
...
2.27.10 AstPassMacro
AstPassMacro is one macro to rule them all. It gets entire module as an input, and can be invoked at numerous
passes:
class AstPassMacro
def abstract apply ( prog:ProgramPtr; mod:Module? ) : bool
2.27. Macros 73
daScript Reference Manual, Release 0.2 beta
add_new_dirty_infer_macro adds a pass macro to the dirty section of infer pass. The [dirty_infer]
annotation accomplishes the same thing.
Typically, such macros create an AstVisitor which performs the necessary transformations.
2.27.11 AstTypeInfoMacro
class AstTypeInfoMacro
def abstract getAstChange ( expr:smart_ptr<ExprTypeInfo>; var errors:das_string )
˓→: ExpressionPtr
2.27.12 AstForLoopMacro
class AstForLoopMacro
def abstract visitExprFor ( prog:ProgramPtr; mod:Module?; expr:smart_ptr<ExprFor>
˓→) : ExpressionPtr
2.27.13 AstCaptureMacro
class AstCaptureMacro
def abstract captureExpression ( prog:Program?; mod:Module?; expr:ExpressionPtr;
˓→etype:TypeDeclPtr ) : ExpressionPtr
2.27.14 AstCommentReader
class AstCommentReader
def abstract open ( prog:ProgramPtr; mod:Module?; cpp:bool; info:LineInfo ) : void
def abstract accept ( prog:ProgramPtr; mod:Module?; ch:int; info:LineInfo ) : void
def abstract close ( prog:ProgramPtr; mod:Module?; info:LineInfo ) : void
def abstract beforeStructure ( prog:ProgramPtr; mod:Module?; info:LineInfo ) :
˓→void
2.27. Macros 75
daScript Reference Manual, Release 0.2 beta
2.27.15 AstSimulateMacro
class AstSimulateMacro
def abstract preSimulate ( prog:Program?; ctx:Context? ) : bool
def abstract simulate ( prog:Program?; ctx:Context? ) : bool
preSimulate occurs after the context has been simulated, but before all the structure and function annotation
simulations.
simulate occurs after all the structure and function annotation simulations.
2.27.16 AstVisitor
AstVisitor implements the visitor pattern for the daScript expression tree. It contains a callback for every single
expression in prefix and postfix form, as well as some additional callbacks:
class AstVisitor
...
// find
def abstract preVisitExprFind(expr:smart_ptr<ExprFind>) : void //
˓→prefix
...
Postfix callbacks can return expressions to replace the ones passed to the callback.
PrintVisitor from the ast_print example implements the printing of every single expression in daScript syntax.
make_visitor creates a visitor adapter from the class, derived from AstVisitor. The adapter then can be
applied to a program via the visit function:
If an expression needs to be visited, and can potentially be fully substituted, the visit_expression function
should be used:
2.28 Reification
Expression reification is used to generate AST expression trees in a convenient way. It provides a collection of escaping
sequences to allow for different types of expression substitutions. At the top level, reification is supported by multiple
call macros, which are used to generate different AST objects.
Reification is implemented in daslib/templates_boost.
What happens here is that call to macro qmacro_function generates a new function named madd. The arguments
and body of that function are taken from the block, which is passed to the function. The escape sequence $i takes its
argument in the form of a string and converts it to an identifier (ExprVar).
Reification macros are similar to quote expressions because the arguments are not going through type inference.
Instead, an ast tree is generated and operated on.
qmacro
qmacro is the simplest reification. The input is returned as is, after escape sequences are resolved:
prints:
(2+2)
qmacro_block
qmacro_block takes a block as an input and returns unquoted block. To illustrate the difference between qmacro
and qmacro_block, let’s review the following example:
2.28. Reification 77
daScript Reference Manual, Release 0.2 beta
ExprMakeBlock
ExprBlock
This is because the block sub-expression is decorated, i.e. (ExprMakeBlock(ExprBlock (. . . ))), and qmacro_block
removes such decoration.
qmacro_expr
qmacro_expr takes a block with a single expression as an input and returns that expression as the result. Certain
expressions like return and such can’t be an argument to a call, so they can’t be passed to qmacro directly. The work
around is to pass them as first line of a block:
prints:
return 13
qmacro_type
qmacro_type takes a type expression (type<. . . >) as an input and returns the subtype as a TypeDeclPtr, after re-
solving the escape sequences. Consider the following example:
TypeDeclPtr foo is passed as a reification sequence to qmacro_type, and a new pointer type is generated. The
output is:
int?
qmacro_function
qmacro_function takes two arguments. The first one is the generated function name. The second one is a block
with a function body and arguments. By default, the generated function only has the FunctionFlags generated flag set.
qmacro_variable
qmacro_variable takes a variable name and type expression as an input, and returns the variable as a VariableDe-
clPtr, after resolving the escape sequences:
prints:
foo:int
Reification provides multiple escape sequences, which are used for miscellaneous template substitution.
$i(ident)
$i takes a string or das_string as an argument and substitutes it with an identifier. An identifier can be
substituted for the variable name in both the variable declaration and use:
prints:
$f(field-name)
prints:
foo.fieldname = 13
$v(value)
$v takes any value as an argument and substitutes it with an expression which generates that value. The value does
not have to be a constant expression, but the expression will be evaluated before its substituted. Appropriate make
infrastructure will be generated:
prints:
[[1,2f,"3"]]
In the example above, a tuple is substituted with the expression that generates this tuple.
2.28. Reification 79
daScript Reference Manual, Release 0.2 beta
$e(expression)
$e takes any expression as an argument in form of an ExpressionPtr. The expression will be substituted as-is:
prints:
$b(array-of-expr)
prints:
print(string_builder(0, "\n"))
print(string_builder(1, "\n"))
print(string_builder(2, "\n"))
$a(arguments)
prints:
somefunnycall(1,1 + 2,"foo",2)
Note how the other arguments of the function are preserved, and multiple arguments can be substituted at the same
time.
Arguments can be substituted in the function declaration itself. In that case $a expects array<VariablePtr>:
prints:
def public add ( a:int const; var v1:int; var v2:float = 1.2f; b:int const ) : int
return a + b
$t(type)
$t takes a TypeDeclPtr as an input and substitutes it with the type expression. In the following example:
$c(call-name)
$c takes a string or das_string as an input, and substitutes the call expression name:
prints:
somefunnycall(1,2)
2.29 Context
daScript environments are organized into contexts. Compiling daScript program produces the ‘Program’ object, which
can then be simulated into the ‘Context’.
Context consists of
• name and flags
• functions code
• global variables data
• shared global variable data
• stack
• dynamic memory heap
2.29. Context 81
daScript Reference Manual, Release 0.2 beta
Through its lifetime Context goes through the initialization and the shutdown. Context initialization is implemented in
Context::runInitScript and shutdown is implemented in Context::runShutdownScript. These functions are called auto-
matically when Context is created, cloned, and destroyed. Depending on the user application and the CodeOfPolicies,
they may also be called when Context::restart or Context::restartHeaps is called.
Its initialized in the following order.
1. All global variables are initialized in order they are declared per module.
2. All functions tagged as [init] are called in order they are declared per module, except for specifically
ordered ones.
3. All specifically ordered functions tagged as [init] are called in order they appear after the topological sort.
The topological sort order for the init functions is specified in the init annotation.
• tag attribute specifies that function will appear during the specified pass
• before attribute specifies that function will appear before the specified pass
• after attribute specifies that function will appear after the specified pass
Consider the following example:
[init(before="middle")]
def a
order |> push("a")
[init(tag="middle")]
def b
order |> push("b")
[init(tag="middle")]
def c
order |> push("c")
[init(after="middle")]
def d
order |> push("d")
3. a
Context shuts down runs all functions marked as [finalize] in the order they are declared per module.
For each module which contains macros individual context is created and initialized. On top of regular functions,
functions tagged as [macro] or [_macro] are called during the initialization.
Functions tagged as [macro_function] are excluded from the regular context, and only appear in the macro contexts.
Unless macro module is marked as shared, it will be shutdown after the compilation. Shared macro modules are
initialized during their first compilation, and are shut down during the environment shutdown.
2.29.3 Locking
Context contains recursive_mutex, and can be specifically locked and unlocked with the lock_context or
lock_this_context RAII block. Cross context calls invoke_in_context automatically lock the target context.
2.29.4 Lookups
Global variables and functions can be looked up by name or by mangled name hash on both daScript and C++ side.
2.30 Locks
There are several locking mechanisms available in daScript. They are designed to ensure runtime safety of the code.
Context can be locked and unlocked via lock and unlock functions from the C++ side. When locked Context can not
be restarted. tryRestartAndLock restarts context if its not locked, and then locks it regardless. The main reason to lock
context is when data on the heap is accessed externally. Heap collection is safe to do on a locked context.
Array or Table can be locked and unlocked explicitly. When locked, they can’t be modified. Calling resize, reserve,
push, emplace, erase, etc on the locked Array` will cause panic. Accessing locked Table elements via [] operation
would cause panic.
Arrays are locked when iterated over. This is done to prevent modification of the array while it is being iterated over.
keys and values iterators lock Table as well. Tables are also locked during the find* operations.
2.30. Locks 83
daScript Reference Manual, Release 0.2 beta
Array and Table lock checking occurs on the data structures, which internally contain other Arrays or Tables.
Consider the following example:
The resize operation on the a array will cause panic because a[0] is locked during the iteration. This test, however,
can only happen in runtime. The compiler generates custom resize code, which verifies locks:
_builtin_verify_locks(Arr)
__builtin_array_resize(Arr,newSize,24,__context__)
The _builtin_verify_locks iterates over provided data, and for each Array or Table makes sure it does not lock. If its
locked panic occurs.
Custom operations will only be generated, if the underlying type needs lock checks.
Here are the list of operations, which perform lock check on the data structures::
• a <- b
• return <- a
• resize
• reserve
• push
• push_clone
• emplace
• pop
• erase
• clear
• insert
• a[b] for the Table
Lock checking can be explicitly disabled
• for the Array or the Table by using set_verify_array_locks and set_verify_table_locks functions.
• for a structure type with the [skip_field_lock_check] structure annotation
• for the entire function with the [skip_lock_check] function annotation
• for the entire context with the options skip_lock_checks
• for the entire context with the set_verify_context_locks function
THREE
The Virtual Machine of daScript consists of a small execution context, which manages stack and heap allocation. The
compiled program itself is a Tree of “Nodes” (called SimNode) which can evaluate virtual functions. These Nodes
don’t check argument types, assuming that all such checks were done by compiler. Why tree-based? Tree-based
interpreters are slow!
Yes, Tree-based interpreters are infamous for their low performance compared to byte-code interpreters.
However, this opinion is typically based on a comparison between AST (abstract syntax tree) interpreters of dy-
namically typed languages with optimized register- or stack-based optimized interpreters. Due to their simplicity of
implementation, AST-based interpreters are also seen in a lot of “home-brewed” naive interpreters, giving tree-based
interpreters additional bad fame. The AST usually contains a lot of data that is useless or unnecessary for interpreta-
tion, and big tree depth and complexity.
It is also hard to even make an AST interpreter of a statically typed language which will somehow benefit from
statically typed data - basically each tree node visitor will still return both the value and type information in generic
form.
In comparison, a good byte-code VM interpreter of a typical dynamically typed language will feature a tight core
loop of all or the most frequent instructions (probably with computed goto) and additionally can statically (or during
execution) infer types and optimize code for it.
Register- and stack-based- VMs each have their own trade-offs, notably with generally fewer generated instruc-
tions/fused instructions, fewer memory moves/indirect memory access for register-based VMs, and smaller instruction
sets and easier implementation for stack-based VMs.
The more “core types” the VM has, the more instructions will probably be needed in the instruction set and/or the
instruction cost increases. Although dynamically typed languages usually don’t have many core types, and some
can embed all their main type’s values and type information in just 64bits (using NAN-tagging, for example), that
still usually leaves one of these core types (table/class/object) to be implemented with associative containers lookups
(unordered_map/hashmap). That is not optimal for cache locality/performance, and also makes interop with host
(C++) types slow and inefficient.
Interop with host/C++ functions because of that is usually slow, as it requires complex and slow arguments/return
value type conversion, and/or associative table(s) lookup.
So, typically, host functions calls are very “heavy”, and programmers usually can’t optimize scripts by extracting just
some of functionality into C++ function - they have to re-write big chunks/loops.
Increasing the amount of core internal types can help (for example, making “float3”, a typical type in game de-
velopment, one of the “core” types), but this makes instruction set bigger/slower, increases the complexity of type
85
daScript Reference Manual, Release 0.2 beta
conversion, and usually introduces mandatory indirection (due to limited bitsize of value type), which is also not good
for cache locality.
However, daScript does not interpret the AST, nor is it a dynamically typed language.
Instead, for run-time program execution it emits a different tree (Simulation Tree), which doesn’t require type infor-
mation to be provided for arguments/return types, since it is statically typed, and all the types are known.
For the daScript ABI, 128bit words are used, which are natural to most of modern hardware.
The chosen representation helps branch prediction, increases cache locality, and provides a mix of stack and register
based code - each ‘Node’ utilizes native machine registers.
It is also important to note that the amount of “types” and “instructions” doesn’t matter much - what matters is the
amount of different instructions used in a particular program/function.
Type conversion between the VM and C++ ABIs is straightforward (and for most of types is as simple as a move
instruction), so it is very fast and cache-friendly.
It also makes it possible for the programmer to optimize particular functionality (in interpretation) by extracting it to
a C++/host function - basically “fusing” instructions into one.
Adding new user-types is rather simple and not painful performance- or engineering-wise.
“Value” types have to fit into 128bits and have to be relocatable and zero-initialized (i.e. should be trivially destructible,
and allow memcpy and memsetting with zeroes); all other types are “RefTypes” or “Boxed Types”, which means they
can be operated on in the script only as references/pointers.
There are no limits on the amount of user types, neither is there a performance impact caused by using such types
(besides the obvious cost of indirection for Boxed/Ref Types).
Using generalized nodes additionally allows for a seamless mix of interpretation and Ahead of Time compiled code
in the run-time - i.e. if some of the functions in the script were changed, the unchanged portion would still be running
the optimized AoT code.
These are the main reasons why tree-based interpretation (not to be confused with AST-based) was chosen for the
daScript interpreter, and why its interpreter is faster than most, if not all, byte code based script interpreters.
The daScript Execution Context is light-weight. It basically consists of stack allocation and two heap allocators (for
strings and everything else). One Context can be used to execute different programs; however, if the program has any
global state in the heap, all calls to the program have to be done within the same Context.
It is possible to call stop-the-world garbage collection on a Context (this call is better to be done outside the program
execution; it’s unsafe otherwise).
However, the cost of resetting context (i.e. deallocate all memory) is extremely low, and (depending on memory usage)
can be as low as several instructions, which allows the simplest and fastest form of memory management for all of the
stateless scripts - just reset the context each frame or each call. This basically turns Context heap management into
form of ‘bump/stack allocator’, significantly simplifying and optimizing memory management.
There are certain ways (including code of policies) to ensure that none of the scripts are using global variables at all,
or at least global variables which require heap memory.
For example, one can split all contexts into several cateories: one context for all stateless script programs, and one
context for each GC’ed (or, additionally, unsafe) script. The stateless context is then reset as often as needed (for
example, each ‘top’ call from C++ or each frame/tick), and on GC-ed contexts one can call garbage collection as soon
as it is needed (using some heurestics of memory usage/performance).
Each context can be used only in one thread simultaneously, i.e. for multi-threading you will need one Context for
each simultaneously running thread.
To exchange data/communicate between different contexts, use ‘channels’ or some other custom-brewed C++ hosted
code of that kind.
Specify the AOT type and provide a prefix with C++ includes (see AOT):
Register the module at the bottom of the C++ file using the REGISTER_MODULE or
REGISTER_MODULE_IN_NAMESPACE macro:
REGISTER_MODULE_IN_NAMESPACE(Module_FIO,das);
Use the NEED_MODULE macro during application initialization before the daScript compiler is invoked:
NEED_MODULE(Module_FIO);
Its possible to have additional daScript files that accompany the builtin module, and have them compiled at initializa-
tion time via the compileBuiltinModule function:
Module_FIO() : Module("fio") {
...
// add builtin module
compileBuiltinModule("fio.das",fio_das, sizeof(fio_das));
What happens here is that fio.das is embedded into the executable (via the XXD utility) as a string constant.
Once everything is registered in the module class constructor, it’s a good idea to verify that the module is ready for
AOT via the verifyAotReady function. It’s also a good idea to verify that the builtin names are following the
correct naming conventions and do not collide with keywords via the verifyBuiltinNames function:
Module_FIO() : Module("fio") {
...
// lets verify all names
uint32_t verifyFlags = uint32_t(VerifyBuiltinFlags::verifyAll);
verifyFlags &= ~VerifyBuiltinFlags::verifyHandleTypes; // we skip annotatins due
˓→to FILE and FStat
verifyBuiltinNames(verifyFlags);
// and now its aot ready
verifyAotReady();
}
3.2.2 ModuleAotType
addConstant(*this,"PI",(float)M_PI);
The constant’s type is automatically inferred, assuming type cast infrastructure is in place (see cast).
addEnumeration(make_smart<EnumerationGooEnum>());
addEnumeration(make_smart<EnumerationGooEnum98>());
For this to work, the enumeration adapter has to be defined via the DAS_BASE_BIND_ENUM or
DAS_BASE_BIND_ENUM_98 C++ preprocessor macros:
namespace Goo {
enum class GooEnum {
regular
(continues on next page)
enum GooEnum98 {
soft
, hard
};
}
Custom data types and type annotations can be exposed via the addAnnotation or addStructure functions:
addAnnotation(make_smart<FileAnnotation>(lib));
Custom macros of different types can be added via addAnnotation, addTypeInfoMacro, addReaderMacro,
addCallMacro, and such. It is strongly preferred, however, to implement macros in daScript.
See macros for more details.
Functions can be exposed to the builtin module via the addExtern and addInterop routines.
addExtern
addExtern exposes standard C++ functions which are not specifically designed for daScript interop:
addExtern<DAS_BIND_FUN(builtin_fprint)>(*this, lib, "fprint",
˓→SideEffects::modifyExternal, "builtin_fprint");
Here, the builtin_fprint function is exposed to daScript and given the name fprint. The AOT name for the function is
explicitly specified to indicate that the function is AOT ready.
The side-effects of the function need to be explicitly specified (see Side-effects). It’s always safe, but inefficient, to
specify SideEffects::worstDefault.
Let’s look at the exposed function in detail:
void builtin_fprint ( const FILE * f, const char * text, Context * context ) {
if ( !f ) context->throw_error("can't fprint NULL");
if ( text ) fputs(text,(FILE *)f);
}
C++ code can explicitly request to be provided with a daScript context, by adding the Context type argument. Making
it last argument of the function makes context substitution transparent for daScript code, i.e. it can simply call:
daScript strings are very similar to C++ char *, however null also indicates empty string. That’s the reason the fputs
only occurs if text is not null in the example above.
Let’s look at another integration example from the builtin math module:
addExtern<DAS_BIND_FUN(float4x4_translation), SimNode_ExtFuncCallAndCopyOrMove>(*this,
˓→ lib, "translation",
SideEffects::none, "float4x4_translation")->arg("xyz");
Here, the float4x4_translation function returns a ref type by value, i.e. float4x4. This needs to be in-
dicated explicitly by specifying a templated SimNode argument for the addExtern function, which is
SimNode_ExtFuncCallAndCopyOrMove.
Some functions need to return a ref type by reference:
addInterop
For some functions it may be necessary to access type information as well as non-marshalled data. Interop functions
are designed specifically for that purpose.
Interop functions are of the following pattern:
They receive a context, calling node, and arguments. They are expected to marshal and return results, or v_zero().
addInterop exposes C++ functions, which are specifically designed around daScript:
addInterop<
builtin_read, // function to register
int, // function return type
const FILE*,vec4f,int32_t // function arguments in order
>(*this, lib, "_builtin_read",SideEffects::modifyExternal, "builtin_read");
The interop function registration template expects a function name as its first template argument, function return value
as its second, with the rest of the arguments following.
When a function’s argument type needs to remain unspecified, an argument type of vec4f is used.
Let’s look at the exposed function in detail:
Argument types can be accessed via the call->types array. Argument values and return value are marshalled via cast
infrastructure (see cast).
The daScript compiler is very much an optimizin compiler and pays a lot of attention to functions’ side-effects.
On the C++ side, enum class SideEffects contains possible side effect combinations.
none indicates that a function is pure, i.e it has no side-effects whatsoever. A good example would be purely com-
putational functions like cos or strlen. daScript may choose to fold those functions at compilation time as well as
completely remove them in cases where the result is not used.
Trying to register void functions with no arguments and no side-effects causes the module initialization to fail.
unsafe indicates that a function has unsafe side-effects, which can cause a panic or crash.
userScenario indicates that some other uncategorized side-effects are in works. daScript does not optimize or
fold those functions.
modifyExternal indicates that the function modifies state, external to daScript; typically it’s some sort of C++
state.
accessExternal indicates that the function reads state, external to daScript.
modifyArgument means that the function modifies one of its input parameters. daScript will look into non-constant
ref arguments and will assume that they may be modified during the function call.
Trying to register functions without mutable ref arguments and modifyArgument side effects causes module ini-
tialization to fail.
accessGlobal indicates that that function accesses global state, i.e. global daScript variables or constants.
invoke indicates that the function may invoke another functions, lambdas, or blocks.
daScript provides machinery to specify custom file access and module name resolution.
Default file access is implemented with the FsFileAccess class.
File access needs to implement the following file and name resolution routines:
getNewFileInfo provides a file name to file data machinery. It returns null if the file is not found.
getModuleInfo provides a module name to file name resolution machinery. Given require string req and the
module it was called from, it needs to fully resolve the module:
struct ModuleInfo {
string moduleName; // name of the module (by default tail of req)
string fileName; // file name, where the module is to be found
(continues on next page)
};
3.2.10 Project
Projects need to export a module_get function, which essentially implements the default C++ getModuleInfo
routine:
require strings
require daslib/strings_boost
typedef
module_info = tuple<string;string;string> const // mirror of C++ ModuleInfo
[export]
def module_get(req,from:string) : module_info
let rs <- split_by_chars(req,"./") // split request
var fr <- split_by_chars(from,"/")
let mod_name = rs[length(rs)-1]
if length(fr)==0 // relative to local
return [[auto mod_name, req + ".das", ""]]
elif length(fr)==1 && fr[0]=="daslib" // process `daslib` prefix
return [[auto mod_name, "{get_das_root()}/daslib/{req}.das", ""]]
else
pop(fr)
for se in rs
push(fr,se)
let path_name = join(fr,"/") + ".das" // treat as local path
return [[auto mod_name, path_name, ""]]
The implementation above splits the require string and looks for recognized prefixes. If a module is requested from
another module, parent module prefixes are used. If the root daslib prefix is recognized, modules are looked for from
the get_das_root path. Otherwise, the request is treated as local path.
3.3.1 Cast
ABI infrastructure is implemented via the C++ cast template, which serves two primary functions:
• Casting from C++ to daScript
• Casting to C++ from daScript
The from function expects a daScript type as an input, and outputs a vec4f.
The to function expects a vec4f, and outputs a daScript type.
Let’s review the following example:
template <>
struct cast <int32_t> {
static __forceinline int32_t to ( vec4f x ) { return v_extract_xi(v_
˓→cast_vec4i(x)); }
};
It implements the ABI for the int32_t, which packs an int32_t value at the beginning of the vec4f using multiplatform
intrinsics.
Let’s review another example, which implements default packing of a reference type:
template <typename TT>
struct cast <TT &> {
static __forceinline TT & to ( vec4f a ) { return *(TT *) v_extract_
˓→ptr(v_cast_vec4i((a))); }
};
When C++ types are exposed to daScript, type factory infrastructure is employed.
To expose any custom C++ type, use the MAKE_TYPE_FACTORY macro, or the
MAKE_EXTERNAL_TYPE_FACTORY and IMPLEMENT_EXTERNAL_TYPE_FACTORY macro pair:
MAKE_TYPE_FACTORY(clock, das::Time)
The example above tells daScript that the C++ type das::Time will be exposed to daScript with the name clock.
Let’s look at the implementation of the MAKE_TYPE_FACTORY macro:
#define MAKE_TYPE_FACTORY(TYPE,CTYPE) \
namespace das { \
template <> \
struct typeFactory<CTYPE> { \
(continues on next page)
What happens in the example above is that two templated policies are exposed to C++.
The typeName policy has a single static function name, which returns the string name of the type.
The typeFactory policy creates a smart pointer to daScript the das::TypeDecl type, which corresponds to C++
type. It expects to find the type somewhere in the provided ModuleLibrary (see Modules).
template <>
struct typeFactory<Point3> {
static TypeDeclPtr make(const ModuleLibrary &) {
auto t = make_smart<TypeDecl>(Type::tFloat3);
t->alias = "Point3";
t->aotAlias = true;
return t;
}
};
template <> struct typeName<Point3> { constexpr static const char * name() { return
˓→"Point3"; } };
In the example above, the C++ application already has a Point3 type, which is very similar to daScript’s float3.
Exposing C++ functions which operate on Point3 is preferable, so the implementation creates an alias named Point3
which corresponds to the das Type::tFloat3.
Sometimes, a custom implementation of typeFactory is required to expose C++ to a daScript type in a more native
fashion. Let’s review the following example:
struct SampleVariant {
int32_t _variant;
union {
int32_t i_value;
float f_value;
char * s_value;
};
};
template <>
struct typeFactory<SampleVariant> {
static TypeDeclPtr make(const ModuleLibrary & library ) {
auto vtype = make_smart<TypeDecl>(Type::tVariant);
(continues on next page)
vtype->addVariant("f_value", typeFactory<decltype(SampleVariant::f_value)>
˓→::make(library));
vtype->addVariant("s_value", typeFactory<decltype(SampleVariant::s_value)>
˓→::make(library));
// optional validation
DAS_ASSERT(sizeof(SampleVariant) == vtype->getSizeOf());
DAS_ASSERT(alignof(SampleVariant) == vtype->getAlignOf());
DAS_ASSERT(offsetof(SampleVariant, i_value) == vtype->
˓→getVariantFieldOffset(0));
return vtype;
}
};
Here, C++ type SomeVariant matches the daScript variant type with its memory layout. The code above exposes a
C++ type alias and creates a corresponding TypeDecl.
Handled types represent the machinery designed to expose C++ types to daScript.
A handled type is created by deriving a custom type annotation from TypeAnnotation and adding an instance of that
annotation to the desired module. For example:
Module_Math() : Module("math") {
...
addAnnotation(make_smart<float4x4_ann>());
3.4.1 TypeAnnotation
TypeAnnotation contains a collection of virtual methods to describe type properties, as well as methods to imple-
ment simulation nodes for the specific functionality.
canAot returns true if the type can appear in AOT:
isPod and isRawPod specify if a type is plain old data, and plain old data without pointers, respectively:
isRefType specifies the type ABI, i.e. if it’s passed by reference or by value:
canNew, canDelete and canDeletePtr specify if new and delete operations are allowed for the type, as well
as whether a pointer to the type can be deleted:
canSubstitute queries if LSP is allowed for the type, i.e. the type can be downcast:
getSmartAnnotationCloneFunction returns the clone function name for the := operator substitution:
getSizeOf and getAlignOf return the size and alignment of the type, respectively:
makeFieldType and makeSafeFieldType return the type of the specified field (or null if the field is not found):
makeIndexType returns the type of the [] operator, given an index expression (or null if unsupported):
makeIteratorType returns the type of the iterable variable when serving as a for loop source (or null if unsup-
ported):
There are numerous simulate... routines that provide specific simulation nodes for different scenarios:
virtual SimNode * simulateDelete ( Context &, const LineInfo &, SimNode *, uint32_t )
˓→const
virtual SimNode * simulateDeletePtr ( Context &, const LineInfo &, SimNode *, uint32_
˓→t ) const
virtual SimNode * simulateCopy ( Context &, const LineInfo &, SimNode *, SimNode * )
˓→const
virtual SimNode * simulateClone ( Context &, const LineInfo &, SimNode *, SimNode * )
˓→const
virtual SimNode * simulateRef2Value ( Context &, const LineInfo &, SimNode * ) const
virtual SimNode * simulateGetField ( const string &, Context &, const LineInfo &,
˓→const ExpressionPtr & ) const
virtual SimNode * simulateGetFieldR2V ( const string &, Context &, const LineInfo &,
˓→const ExpressionPtr & ) const
virtual SimNode * simulateSafeGetField ( const string &, Context &, const LineInfo &,
˓→const ExpressionPtr & ) const
virtual SimNode * simulateSafeGetFieldPtr ( const string &, Context &, const LineInfo
˓→&, const ExpressionPtr & ) const
virtual SimNode * simulateGetAtR2V ( Context &, const LineInfo &, const TypeDeclPtr &,
const ExpressionPtr &, const ExpressionPtr &,
˓→uint32_t ) const
walk provides custom data walking functionality, to allow for inspection and binary serialization of the type:
3.4.2 ManagedStructureAnnotation
struct Object {
das::float3 pos;
das::float3 vel;
__forceinline float speed() { return sqrt(vel.x*vel.x + vel.y*vel.y + vel.z*vel.
˓→z); }
};
To bind it, we inherit from ManagedStructureAnnotation, provide a name, and register fields and properties:
...
addField and addProperty are used to add fields and properties accordingly. Fields are registered as ref values.
Properties are registered with an offset of -1 and are returned by value:
addField<DAS_BIND_MANAGED_FIELD(pos)>("position","pos");
addField<DAS_BIND_MANAGED_FIELD(vel)>("velocity","vel");
addProperty<DAS_BIND_MANAGED_PROP(speed)>("speed","speed");
Afterwards, we register a type factory and add type annotations to the module:
MAKE_TYPE_FACTORY(Object, Object)
addAnnotation(make_smart<ObjectStructureTypeAnnotation>(lib));
That way, the field of one type can be registered as another type.
Managed structure annotation automatically implements walk for the exposed fields.
3.4.3 DummyTypeAnnotation
DummyTypeAnnotation is there when a type needs to be exposed to daScript, but no contents or operations are
allowed.
That way, the type can be part of other structures, and be passed to C++ functions which require it.
The dummy type annotation constructor takes a daScript type name, C++ type name, its size, and alignment:
DummyTypeAnnotation(const string & name, const string & cppName, size_t sz, size_t al)
Since TypeAnnotation is a strong daScript type, DummyTypeAnnotation allows ‘gluing’ code in daScript
without exposing the details of the C++ types. Consider the following example:
send_unit_to(get_unit(“Ally”), get_unit_pos(get_unit(“Enemy”)))
The result of get_unit is passed directly to send_unit_to, without daScript knowing anything about the unit
type (other than that it exists).
3.4.4 ManagedVectorAnnotation
push(vec, value)
pop(vec)
clear(vec)
resize(vec, newSize)
Vectors also expose the field length which returns current size of vector.
Managed vector annotation automatically implements walk, similar to daScript arrays.
3.4.5 ManagedValueAnnotation
ManagedValueAnnotation is designed to expose C++ POD types, which are passed by value.
It expects type cast machinery to be implemented for that type.
For optimal performance and seamless integration, daScript is capable of ahead of time compilation, i.e. producing
C++ files, which are semantically equivalent to simulated daScript nodes.
The output C++ is designed to be to some extent human readable.
For the most part, daScript produces AOT automatically, but some integration effort may be required for custom types.
Plus, certain performance optimizations can be achieved with additional integration effort.
daScript AOT integration is done on the AST expression tree level, and not on the simulation node level.
3.5.1 das_index
The das_index template is used to provide the implementation of the ExprAt and ExprSafeAt AST nodes.
Given the input type VecT, output result TT, and index type of int32_t, das_index needs to implement the following
functions:
// regular index
static __forceinline TT & at ( VecT & value, int32_t index, Context * __context__
˓→);
static __forceinline const TT & at ( const VecT & value, int32_t index, Context *
˓→__context__ );
// safe index
(continues on next page)
Note that sometimes more than one index type is possible. In that case, implementation for each index type is required.
Note how both const and not const versions are available. Additionally, const and non const versions of the
das_index template itself may be required.
3.5.2 das_iterator
The das_iterator template is used to provide the for loop backend for the ExprFor sources.
Let’s review the following example, which implements iteration over the range type:
template <>
struct das_iterator <const range> {
__forceinline das_iterator(const range & r) : that(r) {}
__forceinline bool first ( Context *, int32_t & i ) { i = that.from; return i!
˓→=that.to; }
The das_iterator template needs to implement the constructor for the specified type, and also the first, next,
and close functions, similar to that of the Iterator.
Both the const and regular versions of the das_iterator template are to be provided:
template <>
struct das_iterator <range> : das_iterator<const range> {
__forceinline das_iterator(const range & r) : das_iterator<const range>(r) {}
};
By default, AOT generated functions expect blocks to be passed as the C++ TBlock class (see Blocks). This creates
significant performance overhead, which can be reduced by AOT template machinery.
Let’s review the following example:
void peek_das_string(const string & str, const TBlock<void,TTemporary<const char *>> &
˓→ block, Context * context) {
vec4f args[1];
args[0] = cast<const char *>::from(str.c_str());
context->invoke(block, args, nullptr);
}
The overhead consists of type marshalling, as well as context block invocation. However, the following template can
be called like this, instead:
Here, the block is templated, and can be called without any marshalling whatsoever. To achieve this, the function
registration in the module needs to be modified:
There are several function annotations which control how function AOT is generated.
The [hybrid] annotation indicates that a function is always called via the full daScript interop ABI (slower), as
oppose to a direct function call via C++ language construct (faster). Doing this removes the dependency between the
two functions in the semantic hash, which in turn allows for replacing only one of the functions with the simulated
version.
The [no_aot] annotation indicates that the AOT version of the function will not be generated. This is useful for
working around AOT code-generation issues, as well as during builtin module development.
Function or type trait expressions can have custom annotations to specify prefix and suffix text around the generated
call. This may be necessary to completely replace the call itself, provide additional type conversions, or perform other
customizations.
Let’s review the following example:
Here, the class info macro converts the requested type information to void *. This part of the class machinery allows
the __rtti pointer of the class to remain void, without including RTTI everywhere the class is included.
ExprField is covered by the following functions in the handled type annotation (see Handles):
By default, prefix functions do nothing, and postfix functions append .fieldName and ->fieldName accordingly.
Note that ExprSafeField is not covered yet, and will be implemented for AOT at some point.