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 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
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 implicit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31
2.10 Table . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.11 Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
2.12 Function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.12.1 Function declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34
2.12.2 OOP-style calls . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
2.12.3 Tail Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.12.4 Operator Overloading . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.12.5 Overloading the ‘.’ and ‘?.’ operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40
2.12.6 Overloading accessors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.13 Modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.13.1 Native modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 42
2.13.2 Builtin modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.13.3 Shared modules . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.13.4 Module function visibility . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.14 Block . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43
2.15 Lambda . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 45
2.15.1 Capture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.15.2 Iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.15.3 Implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.16 Struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.16.1 Struct Declaration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48
2.16.2 Structure Function Members . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.16.3 Inheritance . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.16.4 Alignment . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.16.5 OOP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.17 Tuple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.18 Variant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.18.1 Alignment and data layout . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
ii
2.19 Class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.19.1 Implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56
2.20 Constants, Enumerations, Global variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.20.1 Constant . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.20.2 Global variable . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.20.3 Enumeration . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.21 Bitfield . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.22 Comprehension . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.23 Iterator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60
2.23.1 builtin iterators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.23.2 builtin iteration functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
2.23.3 low level builtin iteration functions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.23.4 next implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
2.24 Generator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63
2.24.1 implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64
2.25 Finalizer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
2.25.1 Rules and implementation details . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
2.26 String Builder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.27 Generic Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
2.27.1 typeinfo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.27.2 auto and auto(named) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
2.27.3 type contracts and type operations . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
2.27.4 options . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
2.28 Macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
2.28.1 Compilation passes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72
2.28.2 Invoking macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.28.3 AstFunctionAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
2.28.4 AstBlockAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.28.5 AstStructureAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 75
2.28.6 AstEnumerationAnnotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.28.7 AstVariantMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
2.28.8 AstReaderMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
2.28.9 AstCallMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
2.28.10 AstPassMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
2.28.11 AstTypeInfoMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.28.12 AstForLoopMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.28.13 AstCaptureMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
2.28.14 AstCommentReader . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
2.28.15 AstSimulateMacro . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
2.28.16 AstVisitor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
2.29 Reification . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.29.1 Simple example . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.29.2 Quote macros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
2.29.3 Escape sequences . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 84
2.30 Pattern matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86
2.30.1 Enumeration Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.30.2 Matching Variants . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 87
2.30.3 Declaring Variables in Pattern Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
2.30.4 Matching Structs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
2.30.5 Using Guards . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.30.6 Tuple Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 89
2.30.7 Matching Static Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
2.30.8 Dynamic Array Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.30.9 Match Expressions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
iii
2.30.10 Matching with || Expression . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2.30.11 [match_as_is] Structure Annotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
2.30.12 [match_copy] Structure Annotation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
2.30.13 Static Matching . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
2.30.14 match_type . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2.30.15 Multi-Match . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
2.31 Context . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
2.31.1 Initialization and shutdown . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97
2.31.2 Macro contexts . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.31.3 Locking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.31.4 Lookups . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.32 Locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.32.1 Context locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.32.2 Array and Table locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
2.32.3 Array and Table lock checking . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 98
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.
If inscope is specified, the delete id statement is added in the finally section of the block, where the variable is
declared. It can’t appear directly in the loop block, since finally section of the loop is executed only once.
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 implicit
implicit keyword is used to specify that type can be either temporary or regular type, and will be treated as defined.
For example:
def foo ( a : Foo implicit ) // a will be treated as Foo, but will also accept Foo
˓→# as argument
def foo ( a : Foo# implicit ) // a will be treated as Foo#, but will also accept
˓→Foo as argument
Unfortunately implicit conversions like this are unsafe, so implicit is unsafe by definition.
2.9. implicit 31
daScript Reference Manual, Release 0.2 beta
2.10 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:
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.11 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:
2.11. Array 33
daScript Reference Manual, Release 0.2 beta
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:
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.12 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
2.12. Function 35
daScript Reference Manual, Release 0.2 beta
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
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
2.12. Function 37
daScript Reference Manual, Release 0.2 beta
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.
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):
[expect_any_tuple(blah) || expect_any_variant(blah)]
def print_blah ...
In the example above print_blah will accept any tuple or variant. Available logic operations are !, &&, || and ^^.
LSP can be explicitly prohibited for a particular function argument via the explicit keyword:
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).
2.12. Function 39
daScript Reference Manual, Release 0.2 beta
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.
daScript allows you to overload operators, which means that you can define custom behavior for operators when used
with your own data types. To overload an operator, you need to define a special function with the name of the operator
you want to overload. Here’s the syntax:
In this syntax, <operator> is the name of the operator you want to overload (e.g. +, -, *, /, ==, etc.), <arguments> are
the parameters that the operator function takes, and <return_type> is the return type of the operator function.
For example, here’s how you could overload the == operator for a custom struct called iVec2:
struct iVec2:
x, y: int
In this example, we define a structure called iVec2 with two integer fields (x and y).
We then define an operator== function that takes two parameters (a and b) of type iVec2. This function returns a bool
value indicating whether a and b are equal. The implementation checks whether the x and y components of a and b
are equal using the == operator.
With this operator overloaded, you can now use the == operator to compare iVec2 objects, like this:
let v1 = iVec2(1, 2)
let v2 = iVec2(1, 2)
let v3 = iVec2(3, 4)
In this example, we create three iVec2 objects and compare them using the == operator. The first comparison (v1 ==
v2) returns true because the x and y components of v1 and v2 are equal. The second comparison (v1 == v3) returns
false because the x and y components of v1 and v3 are not equal.
daScript allows you to overload the dot . operator, which is used to access fields of structure or a class. To overload
the dot . operator, you need to define a special function with the name operator . Here’s the syntax:
In this syntax, <object> is the object you want to access, <type> is the type of the object, <name> is the name of the
field you want to access, and <return_type> is the return type of the operator function.
Operator ?. works in a similar way.
For example, here’s how you could overload the dot . operator for a custom structure called Goo:
struct Goo
a: string
In this example, we define a struct called Goo with a string field called a.
We then define two operator. functions:
The first one takes two parameters (t and name) and returns a string value that contains the name of the field or method
being accessed (name) and the value of the a field of the Goo object (t.a). The second one takes one parameter (t) and
returns the length of the a field of the Goo object (t.a). With these operators overloaded, you can now use the dot .
operator to access fields and methods of a Goo object, like this:
In this example, we create an instance of the Goo struct and access its world field using the dot . operator. The
overloaded operator. function is called and returns the string “world = hello”. We also access the length property of
the Goo object using the dot . operator. The overloaded operator. length function is called and returns the length of
the a field of the Goo object (5 in this case).
The . . syntax is used to access the fields of a structure or a class while bypassing overloaded operations.
daScript allows you to overload accessors, which means that you can define custom behavior for accessing fields of
your own data types. Here is an example of how to overload the accessor for a custom struct called Foo:
struct Foo
dir : float3
def operator . length ( foo : Foo )
return length(foo.dir)
def operator . length := ( var foo:Foo; value:float )
foo.dir = normalize(foo.dir) * value
[export]
def main
var f = [[Foo dir=float3(1,2,3)]]
print("length = {f.length} // {f}\n")
f.length := 10.
print("length = {f.length} // {f}\n")
2.12. Function 41
daScript Reference Manual, Release 0.2 beta
It now has accessor length which can be used to get and set the length of the dir field.
Classes allow to overload accessors for properties as well:
class Foo
dir : float3
def const operator . length
return length(dir)
def operator . length := ( value:float )
dir = normalize(dir) * value
2.13 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:
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:
module Foo shared
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:
let s1 = sin(0.0) // implicit, assumed math::sin
let s2 = math::sin(0.0) // explicit, always math::sin
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.14 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
2.14. Block 43
daScript Reference Manual, Release 0.2 beta
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
passthrough(1) <| $ ( a )
assert(a==1)
passthrough(2) <| $ ( b )
assert(a==1 && b==2)
passthrough(3) <| $ ( c )
assert(a==1 && b==2 && c==3)
pos += vel * dt
2.15 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:
lambda_type ::= lambda { optional_lambda_type }
optional_lambda_type ::= < { optional_lambda_arguments } { : return_type } >
optional_lambda_arguments := ( lambda_argument_list )
lambda_argument_list := argument_name : type | lambda_argument_list ; argument_name :
˓→type
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:
def foo ( x : lambda < (arg1:int;arg2:float&):bool > )
...
var y <- x
...
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.15. Lambda 45
daScript Reference Manual, Release 0.2 beta
2.15.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:
var a1 <- [{int 1;2}]
var a2 <- [{int 1;2}]
var a3 <- [{int 1;2}]
unsafe // required do to capture of a1 by reference
var lam <- @ <| [[&a1,<-a2,:=a3]]
push(a1,1)
push(a2,1)
push(a3,1)
invoke(lam)
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.15.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)
• have boolean return type, where true means continue iteration, and false means stop
A more straightforward way to make iterator is with generators (see Generators).
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
2.15. Lambda 47
daScript Reference Manual, Release 0.2 beta
The lack of a copy or move ensures there are not multiple pointers to a single instance of the captured data floating
around.
2.16 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
the structure itself works as an initializer. The compiler will generate 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:
let fExplicit = [[Foo x=13]] // x = 13, y = 0
The “Clone initializer” is useful pattern for creating a clone of an existing structure when both structures are on the
heap:
def Foo ( p : Foo? ) // "clone initializer" takes pointer to existing
˓→structure
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
2.16. Struct 49
daScript Reference Manual, Release 0.2 beta
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.16.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.16.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.16.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:
var f3d: Foo3D = Foo3D()
(cast<Foo> f3d).y = 5
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")
2.16. Struct 51
daScript Reference Manual, Release 0.2 beta
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.17 Tuple
Two tuple declarations are the same if they have the same number of types, and their respective types are the same:
var a : tuple<int; float>
var b : tuple<i:int; f:float>
a = b
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.18 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 (continues on next page)
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:
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
var t : U_F
assert(variant_index(t)==0)
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))
2.18. Variant 53
daScript Reference Manual, Release 0.2 beta
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.19 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)
def Foo(X,Y:int) // custom initializer
Foo`set(self,X,Y)
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 Foo
dir : float3
def const length
return length(dir) // dir is const float3 here
2.19. Class 55
daScript Reference Manual, Release 0.2 beta
class Foo
dir : float3
def Foo ( x,y,z:float )
dir = float3(x,y,z)
def Foo ( d:float3 )
dir = d
def const operator . length
return length(dir)
def operator . length := ( value:float )
dir = normalize(dir) * value
def const operator + ( other:Foo )
return Foo(dir + other.dir)
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.
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.20.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.20.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.21 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.22 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:
2.21. Bitfield 59
daScript Reference Manual, Release 0.2 beta
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.23 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:
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")
2.23. Iterator 61
daScript Reference Manual, Release 0.2 beta
_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.24 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:
2.24. Generator 63
daScript Reference Manual, Release 0.2 beta
__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__)
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.25 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:
2.25. Finalizer 65
daScript Reference Manual, Release 0.2 beta
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
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).
2.25. Finalizer 67
daScript Reference Manual, Release 0.2 beta
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:
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.27.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
Generic function arguments, result, and inferred type aliases can be operated on during the inference.
const specifies, that constant and regular expressions will be matched:
==const specifies, that const of the expression has to match const of the argument:
def foo ( a : array<auto -const> ) // matches any array, with non-const elements
==& specifies that reference of the expression has to match reference of the argument:
def foo ( a : auto& ==& ) // accepts any type, passed by reference (for example
˓→variable i, even if its integer)
def foo ( a : auto ==& ) // accepts any type, passed by value (for example
˓→value 3)
def foo ( a : auto[] ) // accepts static array of any type of any size
-[] will remove static array dimension from the matching type:
implicit specifies that both temporary and regular types can be matched, but the type will be treated as specified.
implicit is _UNSAFE_:
def foo ( a : Foo implicit ) // accepts Foo and Foo#, a will be treated as Foo
def foo ( a : Foo# implicit ) // accepts Foo and Foo#, a will be treated as Foo#
explicit specifies that LSP will not be applied, and only exact type match will be accepted:
def foo ( a : Foo ) // accepts Foo and any type that is inherited from
˓→Foo directly or indirectly
2.27.4 options
def foo ( a : Bar explicit | Foo ) // first will try to match exactly Bar, than
˓→anything else inherited from Foo
2.28 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
1. If there are any errors, compilation stops
7. Optimization pass repeats itself until no new transformations are reported
1. Built-in optimization pass happens
2. Macro optimization pass happens
8. If there are any errors during optimization passes, compilation stops
9. If the module contains any macros, simulation happens
1. If there are any simulation errors, compilation stops
2. Module macro functions (annotated with _macro) are invoked
1. If there are any errors, compilation stops
Modules are compiled in require order.
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.28.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
2.28. Macros 73
daScript Reference Manual, Release 0.2 beta
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.
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.28.4 AstBlockAnnotation
class AstBlockAnnotation
def abstract apply ( var blk:smart_ptr<ExprBlock>; var group:ModuleGroup;
˓→args:AnnotationArgumentList; var errors : das_string ) : bool
2.28.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.28. Macros 75
daScript Reference Manual, Release 0.2 beta
2.28.6 AstEnumerationAnnotation
class AstEnumerationAnnotation
def abstract apply ( var st:EnumerationPtr; var group:ModuleGroup;
˓→args:AnnotationArgumentList; var errors : das_string ) : bool
2.28.7 AstVariantMacro
class AstVariantMacro
def abstract visitExprIsVariant ( prog:ProgramPtr; mod:Module?; expr:smart_ptr
˓→<ExprIsVariant> ) : ExpressionPtr
if isExpression(expr.value._type)
var vdr <- new [[ExprField() at=expr.at, name:="__rtti", value <- clone_
˓→expression(expr.value)]]
...
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.28.8 AstReaderMacro
class AstReaderMacro
def abstract accept ( prog:ProgramPtr; mod:Module?; expr:ExprReader?; ch:int;
˓→info:LineInfo ) : bool
Reader macros are invoked via the % READER_MACRO_NAME ~ character_sequence syntax. The accept
function notifies the correct terminator of the character sequence:
[reader_macro(name="arr")]
class ArrayReader : AstReaderMacro
def override accept ( prog:ProgramPtr; mod:Module?; var expr:ExprReader?; ch:int;
˓→info:LineInfo ) : bool
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.28. Macros 77
daScript Reference Manual, Release 0.2 beta
2.28.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.28.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.28.11 AstTypeInfoMacro
class AstTypeInfoMacro
def abstract getAstChange ( expr:smart_ptr<ExprTypeInfo>; var errors:das_string )
˓→: ExpressionPtr
2.28.12 AstForLoopMacro
class AstForLoopMacro
def abstract visitExprFor ( prog:ProgramPtr; mod:Module?; expr:smart_ptr<ExprFor>
˓→) : ExpressionPtr
2.28.13 AstCaptureMacro
class AstCaptureMacro
def abstract captureExpression ( prog:Program?; mod:Module?; expr:ExpressionPtr;
˓→etype:TypeDeclPtr ) : ExpressionPtr
2.28. Macros 79
daScript Reference Manual, Release 0.2 beta
2.28.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.28.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.28.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. Macros 81
daScript Reference Manual, Release 0.2 beta
2.29 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:
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
2.29. Reification 83
daScript Reference Manual, Release 0.2 beta
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.
$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>:
2.29. Reification 85
daScript Reference Manual, Release 0.2 beta
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)
In the world of computer programming, there is a concept known as pattern matching. This technique allows us to
take a complex value, such as an array or a variant, and compare it to a set of patterns. If the value fits a certain pattern,
the matching process continues and we can extract specific values from that value. This is a powerful tool for making
our code more readable and efficient, and in this section we’ll be exploring the different ways that pattern matching
can be used in daScript.
In daScript pattern matching is implement via macros in the daslib/match module.
daScript supports pattern matching on enumerations, which allows you to match the value of an enumeration with
specific patterns. You can use this feature to simplify your code by eliminating the need for multiple if-else statements
or switch statements. To match enumerations in daScript, you use the match keyword followed by the enumeration
value, and a series of if statements, each representing a pattern to match. If a match is found, the corresponding code
block is executed.
Example:
enum Color
Black
Red
Green
Blue
In the example, the enum_match function takes a Color enumeration value as an argument and returns a value based
on the matched pattern. The if Color Black statement matches the Black enumeration value, the if Color Red statement
matches the Red enumeration value, and the if _ statement is a catch-all that matches any other enumeration value that
hasn’t been explicitly matched.
Variants in daScript can be matched using the match statement. A variant is a discriminated union type that holds one
of several possible values, each of a different type.
In the example, the IF variant has two possible values: i of type int, and f of type float. The variant_as_match function
takes a value of type IF as an argument, and matches it to determine its type.
The if _ as i statement matches any value and assigns it to the declared variable i. Similarly, the if _ as f statement
matches any value and assigns it to the declared variable f. The final if _ statement matches any remaining values, and
returns “anything”.
Example:
variant IF
i : int
f : float
Variants can be matched in daScript using the same syntax used to create new variants.
Here’s an example:
In the example above, the function variant_match takes a variant v of type IF. The first case matches v if it contains an
i and binds the value of i to a variable i. In this case, the function returns 1. The second case matches v if it contains
an f and binds the value of f to a variable f. In this case, the function returns 2. T he last case matches anything that
doesn’t match the first two cases and returns 0.
In daScript, you can declare variables in pattern matching statements, including variant matching. To declare a vari-
able, use the syntax $v(decl) where decl is the name of the variable being declared. The declared variable is then
assigned the value of the matched pattern.
This feature is not restricted to variant matching, and can be used in any pattern matching statement in daScript. In the
example, the if $v(as_int) statement matches the variant value when it holds an integer and declares a variable as_int to
store the value. Similarly, the if $v(as_float) statement matches the variant value when it holds a floating-point value
and declares a variable as_float to store the value.
Example:
variant IF
i : int
f : float
daScript supports matching structs using the match statement. A struct is a composite data type that groups variables
of different data types under a single name.
In the example, the Foo struct has one member a of type int. The struct_match function takes an argument of type Foo,
and matches it against various patterns.
The first match if [[Foo a=13]] matches a Foo struct where a is equal to 13, and returns 0 if this match succeeds. The
second match if [[Foo a=$v(anyA)]] matches any Foo struct and binds its a member to the declared variable anyA.
This match returns the value of anyA if it succeeds.
Example:
struct Foo
a : int
daScript supports the use of guards in its pattern matching mechanism. Guards are conditions that must be satisfied in
addition to a successful pattern match.
In the example, the AB struct has two members a and b of type int. The guards_match function takes an argument of
type AB, and matches it against various patterns.
The first match if [[AB a=$v(a), b=$v(b)]] && (b > a) matches an AB struct and binds its a and b members to the
declared variables a and b, respectively. The guard condition b > a must also be satisfied for this match to be successful.
If this match succeeds, the function returns a string indicating that b is greater than a.
The second match if [[AB a=$v(a), b=$v(b)]] matches any AB struct and binds its a and b members to the declared
variables a and b, respectively. No additional restrictions are placed on the match by means of a guard. If this match
succeeds, the function returns a string indicating that b is less than or equal to a.
Example:
struct AB
a, b : int
Matching tuples in daScript is done with double square brackets and uses the same syntax as creating a new tuple. The
type of the tuple must be specified or auto can be used to indicate automatic type inference.
Here is an example that demonstrates tuple matching in daScript:
In this example, a tuple A of type tuple<int;float;string> is passed as an argument to the function tuple_match. The
function uses a match statement to match different patterns in the tuple A. The if clauses inside the match statement
use double square brackets to specify the pattern to be matched.
The first pattern to be matched is [[auto 1,_,”3”]]. The pattern matches a tuple that starts with the value 1, followed by
any value, and ends with the string “3”. The _ symbol in the pattern indicates that any value can be matched at that
position in the tuple.
The second pattern to be matched is [[auto 13,. . . ]], which matches a tuple that starts with the value 13. The . . .
symbol in the pattern indicates that any number of values can be matched after the value 13.
The third pattern to be matched is [[auto . . . ,”13”]], which matches a tuple that ends with the string “13”. The . . .
symbol in the pattern indicates that any number of values can be matched before the string “13”.
The fourth pattern to be matched is [[auto 2,. . . ,”2”]], which matches a tuple that starts with the value 2 and ends with
the string “2”.
If none of the patterns match, the _ clause is executed and the function returns 0.
Static arrays in daScript can be matched using the double square bracket syntax, similarly to tuples. Additionally,
static arrays must have their type specified, or the type can be automatically inferred using the auto keyword.
Here is an example of matching a static array of type int[3]:
In this example, the function static_array_match takes an argument of type int[3], which is a static array of three
integers. The match statement uses the double square bracket syntax to match against different patterns of the input
array A.
The first case, [[auto $v(a);$v(b);$v(c)]] && (a+b+c)==6, matches an array where the sum of its three elements is
equal to 6. The matched elements are assigned to variables a, b, and c using the $v syntax.
The next three cases match arrays that start with 0, end with 13, and start and end with 12, respectively. The . . . syntax
is used to match any elements in between.
Finally, the _ case matches any array that does not match any of the other cases, and returns -1 in this case.
Dynamic arrays are used to store a collection of values that can be changed during runtime. In daScript, dynamic
arrays can be matched with patterns using similar syntax as for tuples, but with the added check for the number of
elements in the array.
Here is an example of matching on a dynamic array of integers:
In the code above, the dynamic_array_match function takes a dynamic array of integers as an argument. The match
statement then tries to match the elements in the array against a series of patterns.
The first pattern if [{auto $v(a);$v(b);$v(c)}] && (a+b+c)==6 matches arrays that contain three elements and the sum
of those elements is 6. The $v syntax is used to match and capture the values of the elements in the array. The captured
values can then be used in the condition (a+b+c)==6.
The second pattern if [{int 0;0;0;. . . }] matches arrays that start with three zeros. The . . . syntax is used to match any
remaining elements in the array.
The third pattern if [{int . . . ;1;2}] matches arrays that end with the elements 1 and 2.
The fourth pattern if [{int 0;1;. . . ;2;3}] matches arrays that start with the elements 0 and 1 and end with the elements
2 and 3.
The final pattern if _ matches any array that didn’t match any of the previous patterns.
It is important to note that the number of elements in the dynamic array must match the number of elements in the
pattern for the match to be successful.
In daScript, match expressions allow you to reuse variables declared earlier in the pattern to match expressions later
in the pattern.
Here’s an example that demonstrates how to use match expressions to check if an array of integers is in ascending
order:
In this example, the first element of the array is matched to x. Then, the next two elements are matched using
match_expr and the expression x+1 and x+2 respectively. If all three elements match, the function returns true. If
there is no match, the function returns false.
In daScript, you can use the || expression to match either of the provided options in the order they appear. This is useful
when you want to match a variant based on multiple criteria.
Here is an example of matching with || expression:
struct Bar
a : int
b : float
In this example, the function or_match takes a variant B of type Bar and matches it using the || expression. The first
option matches when the value of a is 1 and b is captured as a variable. The second option matches when the value
of a is 2 and b is captured as a variable. If either of these options match, the value of b is returned. If neither of the
options match, 0.0 is returned.
It’s important to note that for the || expression to work, both sides of the statement must declare the same variables.
The [match_as_is] structure annotation in daScript allows you to perform pattern matching for structures of different
types. This allows you to match structures of different types in a single pattern matching expression, as long as the
necessary is and as operators have been implemented for the matching types.
Here’s an example of how to use the [match_as_is] structure annotation:
[match_as_is]
struct CmdMove : Cmd
override rtti = "CmdMove"
x : float
y : float
In this example, the structure CmdMove is marked with the [match_as_is] annotation, allowing it to participate in
pattern matching:
def operator is CmdMove ( cmd:Cmd )
return cmd.rtti=="CmdMove"
In this example, the necessary is and as operators have been implemented for the CmdMove structure to allow it to
participate in pattern matching. The is operator is used to determine the compatibility of the types, and the as operator
is used to perform the actual type casting.
In the matching_as_and_is function, cmd is matched against the CmdMove structure using the [[CmdMove x=$v(x),
y=$v(y)]] pattern. If the match is successful, the values of x and y are extracted and the sum is returned. If the match
is not successful, the catch-all _ case is matched, and 0.0 is returned.
Note that the [match_as_is] structure annotation only works if the necessary is and as operators have been implemented
for the matching types. In the example above, the necessary is and as operators have been implemented for the
CmdMove structure to allow it to participate in pattern matching.
The [match_copy] structure annotation in daScript allows you to perform pattern matching for structures of different
types. This allows you to match structures of different types in a single pattern matching expression, as long as the
necessary match_copy function has been implemented for the matching types.
Here’s an example of how to use the [match_copy] structure annotation:
[match_copy]
struct CmdLocate : Cmd
override rtti = "CmdLocate"
x : float
y : float
z : float
In this example, the structure CmdLocate is marked with the [match_copy] annotation, allowing it to participate in
pattern matching.
The match_copy function is used to match structures of different types. Here’s an example of the implementation of
the match_copy function for the CmdLocate structure:
In this example, the match_copy function takes two parameters: cmdm of type CmdLocate and cmd of type Cmd. The
purpose of this function is to determine if the cmd parameter is of type CmdLocate. If it is, the function performs a
type cast to CmdLocate using the reinterpret, and assigns the result to cmdm. The function then returns true to indicate
that the type cast was successful. If the cmd parameter is not of type CmdLocate, the function returns false.
In this example, the matching_copy function takes a single parameter cmd of type Cmd. This function performs a
type matching operation on the cmd parameter to determine its type. If the cmd parameter is of type CmdLocate, the
function returns the sum of the values of its x, y, and z fields. If the cmd parameter is of any other type, the function
returns 0.
Note that the [match_copy] structure annotation only works if the necessary match_copy function has been imple-
mented for the matching types. In the example above, the necessary match_copy function has been implemented for
the CmdLocate structure to allow it to participate in pattern matching.
Static matching is a way to match on generic expressions daScript. It works similarly to regular matching, but with
one key difference: when there is a type mismatch between the match expression and the pattern, the match will be
ignored at compile-time, as opposed to a compilation error. This makes static matching robust for generic functions.
The syntax for static matching is as follows:
static_match match_expression
if pattern_1
return result_1
if pattern_2
return result_2
...
if _
return result_default
Here, match_expression is the expression to be matched against the patterns. Each pattern is a value or expression
that the match_expression will be compared against. If the match_expression matches one of the patterns, the corre-
sponding result will be returned. If none of the patterns match, the result_default will be returned. If pattern can’t be
matched, it will be ignored.
Here is an example:
enum Color
red
green
blue
In this example, color is matched against the enumeration values red, green, and blue. If the match expression color
is equal to the enumeration value red, 0 will be returned. If the match expression color is equal to the value of blah, 1
will be returned. If none of the patterns match, -1 will be returned.
Note that match_expr is used to match blah against the match expression color, rather than directly matching blah
against the enumeration value.
If color is not Color first match will fail. If blah is not Color, second match will fail. But the function will always
compile.
2.30.14 match_type
The match_type subexpression in daScript allows you to perform pattern matching based on the type of an expression.
It is used within the static_match statement to specify the type of expression that you want to match.
The syntax for match_type is as follows:
if match_type<Type> expr
// code to run if match is successful
where Type is the type that you want to match and. expr is the expression that you want to match against.
Here’s an example of how to use the match_type subexpression:
def static_match_by_type (what)
static_match what
if match_type<int> $v(expr)
return expr
if _
return -1
In this example, what is the expression that is being matched. If what is of type int, then it is assigned to the variable
$v and the expression expr is returned. If what is not of type int, the match falls through to the catch-all _ case, and -1
is returned.
Note that the match_type subexpression only matches types, and mismatched values are ignored. This is in contrast
to regular pattern matching, where both type and value must match for a match to be successful.
2.30.15 Multi-Match
In daScript, you can use the multi_match feature to match multiple values in a single expression. This is useful when
you want to match a value based on several different conditions.
Here is an example of using the multi_match feature:
def multi_match_test ( a:int )
var text = "{a}"
multi_match a
if 0
text += " zero"
if 1
text += " one"
if 2
text += " two"
if $v(a) && (a % 2 == 0) && (a!=0)
text += " even"
if $v(a) && (a % 2 == 1)
(continues on next page)
In this example, the function multi_match_test takes an integer value a and matches it using the multi_match feature.
The first three options match when a is equal to 0, 1, or 2, respectively. The fourth option matches when a is not equal
to 0 and is an even number. The fifth option matches when a is an odd number. The variable text is updated based on
the matching conditions. The final result is returned as the string representation of text.
It’s important to note that the multi_match feature allows for multiple conditions to be matched in a single expression.
This makes the code more concise and easier to read compared to using multiple match and if statements.
The same example using regular match would look like this:
2.31 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
• dynamic string heap
• constant string heap
• runtime debug information
• locks
• miscellaneous lookup infrastructure
In some sense Context can be viewed as daScript virtual machine. It is the object that is responsible for executing the
code and keeping the state. It can also be viewed as an instance of the class, which methods can be accessed when
marked as [export].
Function code, constant string heap, runtime debug information, and shared global variables are shared between cloned
contexts. That allows to keep relatively small profile for the context instance.
Stack can be optionally shared between multiple contexts of different type, to keep memory profile even smaller.
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")
2.31. Context 97
daScript Reference Manual, Release 0.2 beta
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.31.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.31.4 Lookups
Global variables and functions can be looked up by name or by mangled name hash on both daScript and C++ side.
2.32 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.
Array and Table lock checking occurs on the data structures, which internally contain other Arrays or Tables.
Consider the following example:
var a : array < array<int> >
...
for b in a[0]
a |> resize(100500)
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
2.32. Locks 99
daScript Reference Manual, Release 0.2 beta
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
101
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:
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:
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
string importName; // import name, i.e. module namespace (by default same as
˓→module name)
};
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:
};
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> { \
static TypeDeclPtr make(const ModuleLibrary & library ) { \
return makeHandleType(library,#TYPE); \
} \
}; \
template <> \
struct typeName<CTYPE> { \
constexpr static const char * name() { return #TYPE; } \
}; \
};
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 <>
(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 * simulateGetNew ( Context &, const LineInfo & ) const
virtual SimNode * simulateGetAt ( Context &, const LineInfo &, const TypeDeclPtr &,
const ExpressionPtr &, const ExpressionPtr &, uint32_
˓→t ) 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
static __forceinline TT * safe_at ( VecT * value, int32_t index, Context * );
static __forceinline const TT * safe_at ( const VecT * value, int32_t index,
˓→Context * );
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.