Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Syntax Overview

Dolfin uses indentation-sensitive syntax, like Python. Blocks are opened with a colon (:) and delimited by consistent indentation, tabs or spaces, but never mixed. All lines inside a block must be at the same indentation level.

Comments begin with # and run to the end of the line.


# This is a comment
concept Foo:
  has bar: string  # inline comment

File types

A Dolfin project uses two kinds of files.

package.dlf: one per project, declares the package identity:


package <http://example.com/my-ontology>:
  dolfin_version "1"
  version "0.3.0"
  author "Alice"
  description "A short description"
FieldRequiredDescription
dolfin_versionyesLanguage version to use (currently "1")
versionyesOntology version (semver string)
authornoAuthor name
descriptionnoHuman-readable description of the ontology

*.dlf ontology files: one or more files containing declarations (concepts, enums, properties, rules).


Prefixes

Prefixes bind short aliases to IRI namespaces, enabling interoperability with external vocabularies.


prefix <http://schema.org/> as schema
prefix <http://purl.org/dc/elements/1.1/> as dc

Once declared, a prefix can be used in qualified names:


has address: optional schema.PostalAddress

Prefixes can also be declared in a hierarchical block to share a common path:


prefix com.example:
  Person
  Organization as Org

This is equivalent to:


prefix com.example.Person
prefix com.example.Organization as Org

Concepts

A concept defines a category of things and the attributes they can carry.


concept Person:
  has first_name: one string
  has last_name: one string
  has email: optional string
  has age: optional int

An empty concept (no body) is also valid, useful as a flag or tag:


concept FlaggedForReview

Inheritance

A concept can inherit from one or more parents using sub:


concept Employee:
  sub Person
  has employee_id: one string
  has department: one string

concept Manager:
  sub Employee
  has reports: Employee

Multiple parents are comma-separated:


concept PartTimeEmployee:
  sub Employee, Contractor

Attributes

Each attribute is declared with has:

has <name>: [cardinality] <type>

The type can be a primitive or another concept name:


has count: one int
has owner: optional Person
has tags: string          # zero or more (default cardinality)

Cardinality

Cardinality constrains how many values an attribute can hold.

KeywordMeaning
(none)Any number (zero or more)
anyAny number (zero or more, explicit)
oneExactly one (required)
optionalZero or one
someOne or more
NExactly N (integer literal)
at least NN or more
at most NN or fewer
N..MBetween N and M (inclusive)
N..*At least N (same as at least N)

has name: one string          # required, single value
has nickname: optional string # may be absent
has tags: some string         # at least one
has aliases: string           # any number (default)
has lucky_numbers: 3 int      # exactly three
has phone_numbers: at least 1 string  # one or more
has referees: at most 2 string        # no more than two
has scores: 1..5 float        # between one and five
has comments: 0..* string     # zero or more (explicit range)

Primitive types

TypeDescriptionExamples
stringText"hello", ""
intInteger0, 42, -7
floatFloating-point3.14, -0.5
booleanBooleantrue, false

Temporal types

Four temporal types cover dates, times, timestamps and durations. As range types they name the expected value; as values they use smart literals (see Temporal values).

TypeDescriptionXSD typeValue example
dateCalendar datexsd:datedate(June 1st 2026)
timeWall-clock timexsd:timetime(2:30 PM UTC)
date_timeDate + timexsd:dateTimedate_time(June 1st 2026, 14:30)
durationLength of timexsd:durationduration(1y 6mo)

concept Appointment:
  has scheduled_for: one date_time
  has reminder_lead: optional duration

Temporal values

Temporal values are written as smart literals: a type keyword followed by human-friendly notation in parentheses. The keyword fixes the type; the text inside is parsed into the corresponding XSD value.


fact checkup a Appointment
  scheduled_for date_time(June 1st 2026, 2:30 PM)
  reminder_lead duration(1d)

Dates: date(...)

Natural-language dates, in either field order (month names are case-insensitive, ordinal suffixes optional):


date(June 1st 2026)      # → 2026-06-01
date(1st June 2026)      # → 2026-06-01
date(Jun 1 2026)

Numeric dates are ambiguous, so the field order must be given, either inline with an as mask, or file-wide with @locale:


date(01/06/2026 as d/m/y)   # → 2026-06-01
date(2001-09-11 as y-m-d)   # → 2001-09-11

The separator (/, -, or .) must match between the value and the mask.

Times: time(...)

24-hour by default; AM/PM (case-insensitive) switches to 12-hour. An optional timezone follows as an offset or an abbreviation:


time(14:30)              # → 14:30:00
time(2:30 PM)            # → 14:30:00
time(08:46:00 UTC)       # → 08:46:00+00:00
time(14:30 +02:00)       # → 14:30:00+02:00

Datetimes: date_time(...)

A date part and a time part separated by a comma or a space:


date_time(June 1st 2026, 2:30 PM)
date_time(01/06/2026 14:30 as d/m/y)

Durations: duration(...)

One or more <number><unit> terms, in any order. Distinct unit tokens avoid clashing with quantity units:

Unitymowdhmins
Meansyearsmonthsweeksdayshoursminutesseconds

duration(7d)             # → P7D
duration(1h 40min)       # → PT1H40M
duration(1y 6mo 3d)      # → P1Y6M3D

Closed concepts

An closed concept defines a closed set of named values. Only the declared variants are valid:


concept Status:
  one of:
    Pending
    Active
    Archived

Enumurated values are referenced by name in attributes and rules:


has status: one Status

Properties

A property is a named relationship declared independently of any concept, rather than inside one. It connects a domain type to a range type:


property worksFor: Employee -> Organization

Cardinality can be specified on either side:


property manages: one Manager -> Employee

Properties are used in rule patterns and assertions like any attribute.


Facts

A fact declares a named instance of a concept. Facts use the same indentation-based style as every other Dolfin construct.


fact alice a Person
  first_name "Alice"
  last_name "Chen"
  age 34

The general form is:

fact <id> a <ConceptName>
  <property-name> <value>
  ...
  • <id> is the instance identifier. It can be referenced elsewhere as :<id>.
  • a <ConceptName> asserts the type. Multiple types are comma-separated: fact x a Dog, Neutered.
  • Each property line is <name> <value>, where the name matches a has declaration (or inherited one) on the concept.

Value forms

FormExampleMeaning
String literal"Alice"Text value
Integer literal42Integer value
Float literal3.14Floating-point value
Boolean literaltrueBoolean value
Temporal literaldate(June 1st 2026)Date/time/datetime/duration (smart literal)
Enum valueActiveA one of member, unquoted
Reference:aliceAnother named fact in this module
Anonymous block[ ... ]Inline instance (blank node)
Listv1, v2Multiple values for multi-valued properties

References

A reference points to another fact declaration by its identifier, prefixed with ::


fact acme a Organization
  name "Acme Corp"

fact alice a Person
  first_name "Alice"
  employer :acme

Forward references are allowed; all identifiers are resolved after the full file is parsed.

Anonymous blocks

When a property’s value is a structured object with no need for a global identifier, use an inline block:


fact alice a Person
  address [
    street "12 Main St"
    city "Paris"
    country "France"
  ]

The block’s type is inferred from the property’s declared range. To specify a subtype explicitly, add a SubType as the first line of the block:


fact rex a Animal
  owner [
    a LegalGuardian
    first_name "Bob"
  ]

Multi-valued properties

For properties with cardinality any, some, or at least N, repeat the property name on multiple lines:


fact alice a Person
  phone_numbers "555-0001"
  phone_numbers "555-0002"

Or use a comma-separated list:


fact alice a Person
  phone_numbers "555-0001", "555-0002"

Both forms are equivalent.

Multiple anonymous blocks

Repeating a property with block values works the same way:


fact rex a Dog
  vaccinations [
    vaccine_name "Rabies"
    date_administered "2024-03-15"
  ]
  vaccinations [
    vaccine_name "Distemper"
    date_administered "2024-03-15"
  ]

Scoping and identity

All fact declarations in a module share a flat namespace. An id must be unique within the module. Facts from imported modules are referenced with module.id syntax.


Rules

A rule defines an if-then inference. When all patterns in the match: block hold, the assertions in the then: block are applied.


rule classify_senior:
  match:
    ?p a Person
    ?p age [ >= 65 ]
  then:
    ?p a SeniorPerson

Variables

Variables begin with ?. They bind to values during matching and can be referenced in assertions:


rule link_preferred_contact:
  match:
    ?org a Organization
    ?org primary_contact ?person
  then:
    ?person worksFor ?org

Match patterns

Each line in match: is a pattern. All patterns are combined with implicit AND.

Type pattern: checks that a variable is an instance of a concept:


?x a SomeConcept

Triple pattern: checks that a subject has a property with a given value:


?x someProperty someValue
?x someProperty ?y
?x someProperty "literal"
?x someProperty 42

Constraint block: inline conditions on a value using [...]:


?x age [ > 18 ]
?x status [ = Active ]
?x address [ city "Paris" ]
?x manager [ a Director ]

Multiple constraints in a block are AND-combined:


?x score [ >= 50, <= 100 ]

Constraint blocks can be nested:


?x owner [ address [ country "France" ] ]

Comparison operators

Used inside constraint blocks:

OperatorMeaning
=Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal

Quantifiers

Quantifiers express conditions over collections of matching bindings:

QuantifierMeaning
allEvery binding must satisfy the sub-patterns
noneNo binding may satisfy the sub-patterns
at least NAt least N bindings must satisfy the sub-patterns
at most NAt most N bindings may satisfy the sub-patterns
exactly NExactly N bindings must satisfy the sub-patterns
between N, MBetween N and M bindings (inclusive)

rule require_manager_approval:
  match:
    ?req a Request
    none ?approver:
      ?approver a Manager
      ?req approvedBy ?approver
  then:
    ?req a UnapprovedRequest

An optional constraint block on the quantifier variable filters the set being quantified:


at least 2 ?member [ a SeniorEmployee ]:
  ?member worksIn ?dept

Then assertions

Each line in then: asserts a new fact:

Type assertion: classifies a variable as an instance of a concept:


?x a SomeConcept

Triple assertion: asserts a property relationship:


?x someProperty ?y
?x someProperty "value"

Nested rules

A then: block can contain a nested match:/then: block for conditional sub-inferences:


rule complex_inference:
  match:
    ?x a Foo
  then:
    match:
      ?x bar ?y
    then:
      ?y a Baz

Names and identifiers

Simple names are alphanumeric identifiers (with underscores): Person, first_name, status.

Qualified names use dot-notation to reference names in a namespace: schema.Person, com.example.Thing.

IRIs are enclosed in angle brackets: <http://example.com/Thing>. They can appear as package names and prefix targets.

Variables begin with ?: ?person, ?count.


The @iri_name annotation

The @iri_name annotation overrides the IRI segment derived from a file’s name. It appears at the top of an ontology file, before any declarations:


@iri_name "custom-segment"

concept Foo:
  has bar: string

This is useful when the file name doesn’t match the IRI fragment expected by external systems.


File-level directives

Two directives set defaults for temporal values across the whole file. Like @iri_name they sit at the top of the file; they may appear before or after prefix statements, in any order, but before the first declaration.

@locale

@locale sets the field order (and separator) for numeric dates that carry no inline as mask:


@locale d/m/y

fact bob a Person
  birthDate date(01/06/2026)   # → 2026-06-01, no mask needed

The order is any arrangement of d, m, y (e.g. m/d/y, y-m-d). The separator in the directive must match the one used in numeric dates. Without @locale (and without an inline mask), a numeric date is a parse error.

@timezone

@timezone sets the default timezone applied to any time / date_time value that has no inline offset. It accepts an IANA name, a fixed offset, or UTC/Z:


@timezone Europe/Brussels

fact e a Event
  starts_at time(14:30)        # → 14:30:00+01:00

An inline timezone always wins over @timezone; an inline as mask always wins over @locale. Neither directive affects date or duration values.

IANA names such as Europe/Brussels are resolved from a bundled timezone database, available when the parser is built with the iana-tz feature (on by default). Fixed offsets (+02:00) and abbreviations (CET, UTC) work regardless.