Language Rules
Anonymous types are disallowed directly on plurals. And so the following is an error:
Number
Type...
// Error is generated
: Number
Although a highly unlikely to exist in the real world, there exists a case where a type is added directly to a plural that is the same type as the plural itself. This causes an ambiguity when trying to populate the plural, because it is unclear whether the intent is to populate the plural, or to add a place a type on the plural itself. For example, assume the following code:
Foo
FooA, FooB : Foo
Foos : Foo...
Now, let's assume that we want to attach another statement to Foo...
plurals, and the type of that statement is another Foo:
Foo...
AnotherFoo : Foo
Without any additional language rules, this would create an ambiguity:
Foo...
foo : Foo
Foos : Foo...
// Is this:
// A) adding 'AnotherFoo' to Foos? Or,
// B) subtyping the AnotherFoo type defined on Foo as a FooA?
AnotherFoo : FooA
In this case, scenario A would apply (the AnotherFoo
would just be another element of the Foos
plural). If this were to not be the desired behavior, the work around is to simply to assign AnotherFoo
separately:
Foo...
foo : Foo
Foos : Foo...
Foos...
AnotherFoo : FooA
On-Plural types cannot have an Is-a side. For example, the following generates an error:
Foo
Item
Items : Item...
: Item
: Item
// Error
Item... : Foo
Pluralizing an already pluralized type isn't allowed. For example:
Animal
Animals : Animal...
// Error
Animalses : Animals...
Instead, the same effect of a multi-dimensional plural can be achieved via an intermediate type. For example:
Cell
Row
Cells : Cell...
Table
Rows : Row...
An open proposal to support multi-dimensional plurals is found [here](../Proposals/Multi-Dimensional Arrays.md).
Colons inside regular expressions must be escaped, otherwise, this colon will be recognized as the has-a / is-a separator for the the statement:
/regular\:expression/ : Thing
More formally, the actual parsing rule is that the first non-escaped colon character acts as the statement separator (called the "Joint").
// Error
/[0-9]/ : Number...
A regular expression only matches the exact types stated in it's is-a side of the statement, and nothing further. For example:
String
Number
/[0-9]/ : Number
Foo
Value : String, Number
Bar : Foo
// Error
Value : 123
Remember that the comma operator is an AND operation. String, Number
means String and Number. The value of 123
does not fit the definition because it was defined as a String and a Number, whereas the regular expression was defined as matching only numbers.
Regular expressions always need to match the entire is-a side of a statement after being trimmed. Therefore, the ^
and $
markers within a regular expression don't have any special meaning (because they're implied), and so matching them directly doesn't require them to be escaped. This also means that regular expressions can't match across multiple lines.
Consider the following example:
Number
/\d+/ : Number
Employee
Salary : Number
// 50000 is considered a Number, not a subtype of number
Developer : Employee
Salary : 50000
// And so this is still valid
LeadDeveloper : Developer
Salary : 100000
Regular expressions are special in that they don't create new types. They only provide new ways of refer to a existing types. From a type-compliance point of view, the salary "overrides" from the example above are all the same as saying Salary : Number
. For this reason, content matched by regular expressions on the is-a side of a statement function more like default values than explicit type constraints. To clarify, without this language rule, if is-a side content matched by a regular expression was considered a subtype, then the Salary
of any type extending Developer
would need to be exactly 50000
, which would be undesirable in most cases.
The Truth core only has a very loose notion of encapsulation. When an external Truth file is imported into a document, theoretically all the terms defined in the external file become available to the importing document (subject to the scoping rules). If Truth were to simply copy the standard scoping rules from languages such as JavaScript, this could cause undesirable redeclaration of types, essentially rendering some of the imported types inaccessible. Take for example, the following Truth file that declares information about hotels:
// -- Hotel.truth --
Feature
Tennis Court : Feature
Hot Tub : Feature
Patio : Feature
Pool : Feature
Hotel
Features : Feature...
If an importing file were to re-declare Pool
to have a new meaning, Pool
still needs to remain accessible. Otherwise, it wouldn't be possible to add Pool
as an item in a Hotel's list of features:
Hotel.truth
Game
Pool : Game
Chess : Game
Darts : Game
Hilton : Hotel
Features
// ?
Pool
Patio
The rule adopted by most languages is that the declaration of an identifier with the same name as another identifier from a higher-level scope causes that higher-level identifier to be overridden. The higher-level identifier becomes inaccessible to the current scope, and any scopes nested inside of it. Under this rule, Pool
in this case would be a Game
rather than a Feature
. However, in the context of a list of Features, we're clearly referring to the Pool
like swimming pool rather than Pool
like the game. To address this case, Truth uses a type resolution strategy we call Polymorphic Type Resolution.
Instead of climbing up the scope chain looking for the Pool
identifier, finding that the first definition of Pool
is of the wrong type (Game
, not Feature
), and then proceeding to generate an error, Polymorphic Type Resolution considers all matching terms from the entire scope ancestry, all the way back to the root. It then internally organizes all occurrences of the term in a list. It then selects the instances of the term from this list that complies with the type definition. An error is then generated only after this list has been found to not contain a sufficient number of terms to conform to the definition specified at the call site.
Truth has the concept of Unspecified Types, which are types that exist within a scope implicitly. For example:
Number
Animal
Weight : Number
Rabbit : Animal
// Rabbit actually has a Weight type,
// but it's in the "Unspecified" state.
These Unspecified Types don't affect type resolution. For example:
Number
Decimal : Number
Weight : Number
Animal
// The type of "Weight" is inferred as Number
Weight : Decimal
Rabbit : Animal
// There's actually an unspecified "Weight"
// type here, whose type is "Decimal", which
// is acquired from "Animal"
Head
// This Weight type is inferred as
// Number, not Decimal.
Weight
In most programming languages that support it, multiple inheritance typically has the unfortunate side effect of causing naming conflicts. This occurs when type A inherits from types B and C, but both B and C have members named D. This doesn't occur in Truth, because types that extend from multiple sources are given the union of the statements owned by each source. For example:
A
B
ClassA
Value : A
ClassB
Value : B
// ClassAB has an implicit statement called Value which is of type A, B
ClassAB : ClassA, ClassB
From the example, if Value
defined in both ClassA
and ClassB
were to have the same type, the result is still a union:
A
ClassA
Value : A
ClassB
Value : A
// ClassAB has an implicit statement called Value which is of type A
ClassAB : ClassA, ClassB
Although occasionally undesirable, it's possible to create a recursive statement structure in Truth:
Number
Min : Number
Max : Number
// Means that this possible
Number
Max
Number
Max
Number
Max
// And so on...
If this is undesirable behavior, an Agent working with an additional definition can be used to impose restrictions on specific types to prevent this:
Number
Min : Number, Halted
Max : Number, Halted
// Halt would be recognized by the Agent,
// which would prevent the recursive nature
Truth files reference other Truth files simply by writing out a URI that points to the other file:
External.truth
Animal
Rabbit : Animal
In this case, the compiler looks for a file in the same folder as the local file, and includes it's statements. These are treated as if they were declared on a higher level scope, so declaration merging doesn't occur. For example:
-- File1.truth --
Animal
Rabbit : Animal
-- File2.truth --
File1.truth
Creature
Rabbit : Creature
Rabbit
defined in File2.truth would be declared as a Creature
, not a Creature, Animal
.
In addition to local paths, truth files can reference other files which may be located via a web path. Web paths in Truth use browser-style protocol-relative URIs. For example:
//www.domain.org/External.truth
Animal
Rabbit : Animal
While it is possible to import Truth code contained in other files, doing so cannot create a circular relationship. The two documents described below would result in an error:
// -- File1.truth --
File2.truth
// -- File2.truth --
File1.truth
(Diamond inclusion patterns are still permissible.)
The comment token in Truth is actually two forward slashes followed by a whitespace character:
// Comment
//Not a comment
Truth imposes very few restrictions on type names. The only character that cannot exist within a type name is the comma (,
). However, commas can still be matched within a regular expression if the comma is escaped. For example:
/A\,B/ : C
If the type name contains a colon, or starts with a forward slash, it must be escaped. Take for example the following code, which is entirely valid, due to the escaped colon:
UriScheme
http\: : UriScheme
Request
UriScheme
MyRequest : Request
UriScheme : http: