Full Grammar
There are two kind of files in a dolfin package. The package.dlf
placed at the root of a package folder, it serves as a manifest of the whole package. We give an exemple of such a file here and let the grammar being deduced.
package <http://my.special.iri/of-ontology>:
dolfin_version: 1
version: 1.2.3
author: not only me
author: but also you
description: This package is an example
There are regular dolfin files. The folder in which they are, are used to compose the iri of each ontology and then the iri of each element.
ontology ::=
iri_name_annotation?
header_item*
declaration*
EOF
iri_name_annotation ::=
"@iri_name" ( String | IRI ) NEWLINE
(* Prefixes and file-level directives may appear in any order at the top of
the file, before the first declaration. *)
header_item ::=
prefix_statement
| locale_directive
| timezone_directive
locale_directive ::=
"@locale" date_field_order NEWLINE
timezone_directive ::=
"@timezone" timezone_spec NEWLINE
date_field_order ::= field date_sep field date_sep field (* e.g. d/m/y *)
field ::= "y" | "m" | "d"
date_sep ::= "/" | "-" | "."
timezone_spec ::= iana_name (* e.g. Europe/Brussels *)
| utc_offset (* e.g. +02:00, -05:00 *)
| tz_abbrev (* e.g. UTC, Z, CET *)
prefix_statement ::=
"prefix" prefix_target
prefix_target ::=
qualified_name_or_iri ":" NEWLINE
INDENT
prefix_target+
DEDENT
| qualified_name_or_iri "as" Name NEWLINE
| qualified_name_or_iri NEWLINE
declaration ::=
concept
| property
| rule
| query
| fact
| mapping
| export
(* A concept with no members is written without the colon: `concept Foo` *)
concept ::=
"concept" Name NEWLINE
| "concept" Name ":" NEWLINE
INDENT
concept_member+
DEDENT
concept_member ::=
concept_iri_name
| sub_concept
| equivalent_concept
| concept_condition
| has_property
| key_block
| "one" "of" ":" NEWLINE
INDENT
enum_value+
DEDENT
(* An enum value may fix the concept's key fields:
one of:
black [red 0.0, green 0.0, blue 0.0]
red [red 1.0, green 0.0, blue 0.0]
Its IRI is prefixed with the concept (Color_red). *)
enum_value ::= Name no_comp_constraint_block? NEWLINE
(* Two instances with equal keys are the same individual (owl:sameAs /
owl:hasKey). `key:` + fields is the same as `has key` on each field. A key
field implies `one`: any other cardinality is an error. A multi-valued key
is typed `set of T` or `list of T` (see has_property). *)
key_block ::= "key" ":" NEWLINE INDENT has_property+ DEDENT
concept_iri_name ::= "@iri_name" IRI NEWLINE
sub_concept ::= "sub" type_ref ("," type_ref)* NEWLINE with_block?
(* A with_block needs a single type: `sub T` + block is (T and line and …). *)
equivalent_concept ::= "equivalent to" type_ref ("," type_ref)* NEWLINE with_block?
(* owl:equivalentClass, one per type and per line; same with_block rule as sub. *)
(* Concept conditions (SHACL side). `equivalent to T` + a condition block
defines the concept as T narrowed by the block: members are inferred and
asserted members validated. `check` + a block validates only. The block is
one line, indented under the keyword line (no line continuation inside
brackets); it cannot be mixed with a with_block. `check` is matched
contextually and stays usable as a name; its ":" is optional. *)
concept_condition ::=
"equivalent to" type_ref ("," type_ref)* NEWLINE
INDENT condition_block NEWLINE DEDENT
| "check" ":"? NEWLINE
INDENT condition_block NEWLINE DEDENT
condition_block ::= "[" condition ("," condition)* "]"
condition ::=
"a" type_ref
| comparison_op (field_expr | focus_ref) (* `<= now() - $birthdate` *)
| condition_quantifier? qualified_name variable? condition_block?
| "$" Name variable? (* binds the focus node's field *)
(* Default quantifier: all. `some` = at least one. "at least"/"at most" also
take the underscore spelling. No `between`; a count is an integer or a
variable bound earlier in the block (`[ $ quota ?q, at_least ?q item ]`). *)
condition_quantifier ::=
"all" | "some" | "none"
| ("at least" | "at most" | "exactly") (Integer | variable)
(* `$` is the focus node (SHACL $this), always the outermost one at any
nesting depth. `$age` is shorthand for `$ age ?age`; `$age` and `$ age` lex
the same. Name an intermediate node with a variable:
`[ some of_country ?c [ … ] ]`. `$` only appears in condition blocks. *)
focus_ref ::= "$" | "$" Name | "$" Name variable
(* Example:
concept CanVote:
equivalent to Citizen
[ some of_country [ legal_age_of_vote [ <= $age ]]]
concept Adult:
check
[ $ birthdate ?b, some of_country [ legal_age_of_vote [ <= $ age ?a ]]] *)
with_block ::= INDENT with_line+ DEDENT
with_line ::= with_clause NEWLINE with_block? (* a nested block narrows the filler *)
has_property ::=
"has" "key"? Name ":" (cardinality? | collection) type_ref field_block? NEWLINE
(INDENT has_line+ DEDENT)?
(* A `has` body: property axioms, and `@iri_name <IRI>` (the field's property
IRI, shared by every `has` of that name in the file), in any order. No
domain / range lines. *)
has_line ::=
"@iri_name" IRI NEWLINE
| "sub" qualified_name NEWLINE
| "inverse of" qualified_name NEWLINE
| "transitive" NEWLINE | "symmetric" NEWLINE | "reflexive" NEWLINE
| "equivalent to" property_path NEWLINE
(* `has tags: set of string`: any number of values, no order (a plain
multi-valued property; a set key compares as a set). `has steps: list of
Step`: one rdf:List node (rdfs:range rdf:List), its rdf:first values the
Steps in order. A collection takes no cardinality; type_ref is the element
type, and a field_block constrains the elements. `set` and `list` are
matched contextually (before `of`) and stay usable as names. *)
collection ::= "set" "of" | "list" "of"
(* Value constraints on the field (SHACL side): `has red: float [ <= 1, >= 0 ]`.
An `=` makes a derived field, computed rather than stored:
`has age: duration [ = now() - $birthdate ]`. `now()` (`now` is matched
contextually) and `$name` (the focus node's field) appear in field_block
and condition comparisons only. *)
field_block ::= "[" comparison_op field_expr ("," comparison_op field_expr)* "]"
field_expr ::= expr (* arithmetic over literals, variables, and: *)
| "now" "(" ")"
| "$" Name
property ::=
"property" Name (":" cardinality? type_ref "->" cardinality? type_ref)? NEWLINE
(INDENT property_line+ DEDENT)?
property_line ::=
"@iri_name" IRI NEWLINE (* this property's IRI *)
| "sub" qualified_name NEWLINE
| "inverse of" qualified_name NEWLINE
| "transitive" NEWLINE | "symmetric" NEWLINE | "reflexive" NEWLINE
| "equivalent to" property_path NEWLINE
| "domain" type_ref NEWLINE with_block? (* rdfs:domain *)
| "range" type_ref NEWLINE with_block? (* rdfs:range *)
(* `domain` and `range` are matched contextually: they stay usable as ordinary
names. Body lines come in any order. A side given twice (by the one-line
`D -> R` and a line, or by two lines) is error S014. *)
(* `fact` declares one instance; `mapping` declares a template expanded once
per row of a CSV file or a spreadsheet sheet, or once per `for each` match of
a JSON or XML document. `mapping` is a reserved word, but `from`, `for`,
`each`, `sheet`, `header`, `row`, `map`, `where`, `starts`, `case`, `else`,
`slug` and `to` are matched contextually and stay usable as ordinary names. *)
fact ::=
"fact" Name "a" type_ref ("," type_ref)* NEWLINE
INDENT
fact_assertion+
DEDENT
fact_assertion ::=
qualified_name fact_value ("," fact_value)* NEWLINE
| "is" qualified_name "of" fact_value NEWLINE
| "a" type_ref NEWLINE
fact_value ::=
literal
| ":" Name (* reference to another fact *)
| Name (* enum value *)
| "[" NEWLINE INDENT fact_assertion+ DEDENT "]"
mapping ::=
"mapping" "from" IRI "as" Name NEWLINE
INDENT
mapping_header_line*
"map" ":" NEWLINE
INDENT
source_binding+
DEDENT
where_block?
"to" ":" NEWLINE
INDENT
mapping_template+
DEDENT
DEDENT
(* At most one of each. `for each` is for JSONPath and XPath only; `sheet`
and `header row` (an integer >= 1) are for XLSX and ODS only. *)
mapping_header_line ::=
"for" "each" String NEWLINE (* iteration root *)
| "sheet" String NEWLINE (* worksheet name, matched exactly *)
| "header" "row" Integer NEWLINE
(* A String is a column name under CSV, XLSX and ODS, a JSONPath expression under
JSONPath and an XPath expression under XPath; under the last two a bare
Name is an error. *)
source_binding ::= (Name | String) "as" variable NEWLINE
(* A row failing any condition is dropped silently. *)
where_block ::=
"where" ":" NEWLINE
INDENT
(variable "[" where_condition ("," where_condition)* "]" NEWLINE)+
DEDENT
where_condition ::=
comparison_op (String | Integer | Float)
| "starts" "with" String
mapping_template ::=
"fact" subject_template "a" type_ref ("," type_ref)* NEWLINE
(INDENT mapping_assertion+ DEDENT)?
(* A `{?var}` hole may appear in the subject; a cast there is an error. *)
subject_template ::=
Name ":" template_tail
| prefixed_name template_tail?
| IRI (* holes are written inside the IRI text *)
| Name
template_tail ::= template_hole template_chunk*
(* `{slug(?x)}`: the cell lowercased, accents dropped, other runs of
characters than a-z0-9 replaced by `_`. Also allowed inside an IRI. *)
template_hole ::= "{" variable "}" | "{" "slug" "(" variable ")" "}"
template_chunk ::= template_hole | Name | Integer
mapping_assertion ::=
qualified_name mapping_value ("," mapping_value)* NEWLINE
| "is" qualified_name "of" mapping_value NEWLINE
| "a" type_ref NEWLINE
| qualified_name "case" variable ":" NEWLINE
INDENT
(String "->" fact_value NEWLINE)+
("else" "->" fact_value NEWLINE)?
DEDENT
mapping_value ::=
"{" variable "}" cast?
| "{" variable cast "}" (* same as "{" variable "}" cast *)
| variable cast? (* shorthand for the braced form *)
| subject_template (* an IRI or prefixed name built from cells *)
| "[" NEWLINE INDENT mapping_assertion+ DEDENT "]"
| fact_value (* a constant, identical on every row *)
cast ::= "as" ("string" | "int" | "float" | "boolean" | "date" | quantity_unit)
(* `quantity(kW)`: the cell is the number, the unit is written here. *)
quantity_unit ::= "quantity" "(" unit_expr ("as" convert_target)? ")"
(* `export` writes the rows of a query to a file. Unlike `mapping`, `export`
is not a reserved word: it is matched contextually at the start of a
top-level declaration, so a concept or property named `export` keeps
working. `from`, `map`, `limit`, `to`, `sheet`, `for` and `each` are
contextual too; `query` is the reserved word of `query`. `map:` may be
left out only after `from query`. `header row` is refused under `to:`. *)
export ::=
"export" NEWLINE
INDENT
export_source
export_map? (* required with `from:` *)
"to" ":" NEWLINE
INDENT
IRI "as" Name NEWLINE (* <export://path> as CSV | XLSX | JSONPath | ... *)
mapping_header_line* (* `sheet` and `for each` only *)
DEDENT
DEDENT
export_source ::=
"from" "query" qualified_name NEWLINE
| "from" ":" NEWLINE
INDENT
query_clause*
group_by_block? (* a query body without `return` *)
DEDENT
export_map ::=
"map" ":" NEWLINE
INDENT
export_column+
("limit" Integer NEWLINE)?
DEDENT
export_column ::=
(Name | String) "as" variable cast? NEWLINE
(INDENT column_modifier+ DEDENT)?
rule ::=
"rule" Name ":" NEWLINE
INDENT
match_block
then_block
DEDENT
match_block ::=
"match" ":" NEWLINE
INDENT
match_pattern+
DEDENT
then_block ::=
"then" ":" NEWLINE
INDENT
then_item+
DEDENT
match_pattern ::=
subject qualified_name object NEWLINE
| subject qualified_name contraint_block NEWLINE
| subject "a" tyep_ref NEWLINE
| "among" ":" NEWLINE
INDENT
match_pattern+
DEDENT quantifier ":" NEWLINE
INDENT
match_pattern+
DEDENT
| quantifier ":" NEWLINE
INDENT
match_pattern+
DEDENT
| variable constraint_block NEWLINE (* condition on a bound value *)
| aggregation variable "as" variable NEWLINE (* aggregate, same line as in queries *)
INDENT
match_pattern+ (* no aggregate inside *)
DEDENT
quantifier ::=
"all"
| "none"
| "at least" Integer
| "at most" Integer
| "exactly" Integer
| "between" Integer "," Integer
then_item ::=
assertion NEWLINE
| nested_rule
assertion ::=
subject qualified_name object
| subject qualified_name no_comp_contraint_block
| subject "a" type_ref
nested_rule ::= match_block then_block
(* ----------------------------------------------------------------------- *)
(* Queries *)
(* ----------------------------------------------------------------------- *)
query ::=
"query" Name ":" NEWLINE
INDENT
query_clause*
group_by_block?
return_block?
DEDENT
query_clause ::=
query_subject_pattern
| existence_block
| query_composition
| variable comparison_op (variable | literal) NEWLINE (* filter *)
| aggregation variable "as" variable NEWLINE (* aggregate sub-query *)
INDENT
query_clause+
DEDENT
| "is" qualified_name "of" object NEWLINE
query_subject_pattern ::=
variable "a" type_ref NEWLINE (INDENT property_pattern+ DEDENT)?
| "a" type_ref NEWLINE (INDENT property_pattern+ DEDENT)?
| variable NEWLINE INDENT property_pattern+ DEDENT
| variable qualified_name object NEWLINE (INDENT property_pattern+ DEDENT)?
| variable qualified_name constraint_block NEWLINE
| variable qualified_name subject_block NEWLINE
| variable "is" qualified_name "of" (variable | constraint_block) NEWLINE
property_pattern ::=
qualified_name object NEWLINE
| qualified_name constraint_block NEWLINE
| qualified_name subject_block NEWLINE
| "optional" qualified_name object NEWLINE
| "is" qualified_name "of" (variable | constraint_block) NEWLINE
| "either" qualified_name constraint_block NEWLINE
("or" qualified_name constraint_block NEWLINE)+
subject_block ::= "[" NEWLINE INDENT property_pattern+ DEDENT "]"
existence_block ::=
("some" | "none") ":" NEWLINE
INDENT
query_clause+
DEDENT
(* The field names are the return columns of the called query. *)
query_composition ::=
qualified_name "as" variable NEWLINE
| qualified_name "as" "[" NEWLINE
INDENT
(Name variable NEWLINE)+
DEDENT
"]" NEWLINE?
aggregation ::= "count" | "count" "distinct" | "sum" | "average" | "min" | "max"
(* Each line is an aggregate or a HAVING filter, in any order. *)
group_by_block ::=
"group by" variable NEWLINE
INDENT
( aggregation variable "as" variable NEWLINE
| variable comparison_op (variable | literal) NEWLINE )+
DEDENT
return_block ::=
"return" NEWLINE
INDENT
return_line*
DEDENT
return_line ::=
Name? variable NEWLINE
(INDENT column_modifier+ DEDENT)?
| "limit" Integer NEWLINE
column_modifier ::=
"order" ("asc" | "desc") NEWLINE
| "distinct" NEWLINE
subject ::=
variable
| qualified_name
| constraint_block
object ::=
variable
| literal
| qualified_name
constraint_block ::= "[" constraint ("," constraint)* "]"
no_comp_constraint_block ::= "[" no_comp_constraint ("," no_comp_constraint)* "]"
no_comp_constraint ::=
"a" type_ref
| qualified_name object
| qualified_name constraint_block
(* In rule and query blocks, a comparison takes a literal, a
variable or an arithmetic expression over them; quantifiers and `$` are
condition_block only. *)
constraint ::=
no_comp_constraint
| variable? comparison_op expr
| "is" qualified_name "of" (object | constraint_block)
comparison_op ::=
"="
| "!="
| "<"
| "<="
| ">"
| ">="
type_ref ::=
type_primary
| type_primary with_clause+ (* T and (with …) and … *)
| with_clause+
type_primary ::=
type_atom
| "(" type_ref ")"
| "not" type_primary (* owl:complementOf *)
| "(" type_ref ("or" type_ref)+ ")" (* owl:unionOf *)
| "(" type_ref ("and" type_ref)+ ")" (* owl:intersectionOf *)
(* Mixing "and" and "or" in one group is an error. "and"/"not"/"with"
operands must not contain primitives. *)
with_clause ::= "with" "^"? qualified_name ":" with_filler (* owl:Restriction *)
with_filler ::=
type_primary (* owl:someValuesFrom *)
| "only" type_primary (* owl:allValuesFrom; `only` is contextual *)
| cardinality type_primary (* qualified cardinalities + owl:onClass *)
| "=" (literal | qualified_name) (* owl:hasValue *)
type_atom ::=
"string"
| "int"
| "float"
| "boolean"
| "date"
| "time"
| "date_time"
| "duration"
| quantity_type
| qualified_name
(* Built-in quantity datatypes. Contextual, not keywords: only a type in type
position, so `has length: length` is valid. A capitalised `Mass` or
`unit.Mass` is a qualified_name, linted towards `mass`. "duration" (above)
is the time dimension. *)
quantity_type ::=
"length" | "mass" | "current" | "temperature" | "amount"
| "luminosity" | "speed" | "acceleration" | "force" | "pressure" | "energy"
| "power" | "frequency" | "area" | "volume" | "density" | "momentum"
qualified_name ::=
(* ----------------------------------------------------------------------- *)
(* Literals *)
(* ----------------------------------------------------------------------- *)
literal ::=
String
| Integer
| Float
| Boolean
| IRI
| temporal_literal
| quantity_literal
(* Temporal smart literals. The keyword names the XSD type; the parenthesised
content is human-friendly notation, parsed by the dolfin-datetime library.
The content must NOT contain a ")". *)
temporal_literal ::=
"date" "(" date_content ")"
| "time" "(" time_content ")"
| "date_time" "(" datetime_content ")"
| "duration" "(" duration_content ")"
date_content ::= natural_date | numeric_date ("as" date_field_order)?
natural_date ::= month_name day_ordinal year
| day_ordinal month_name year
month_name ::= "January" | "Jan" | "February" | "Feb" | ... | "December" | "Dec" (* case-insensitive *)
day_ordinal ::= Integer ("st" | "nd" | "rd" | "th")? (* suffix is cosmetic *)
year ::= Integer
numeric_date ::= Integer date_sep Integer date_sep Integer
(* The separator in the value and in the "as" mask must match. Without a mask
the file-level @locale supplies the field order; with neither, an ambiguous
numeric date is an error. *)
time_content ::= hour ":" minute (":" second)? am_pm? timezone_spec?
hour ::= Integer (* 0-23, or 1-12 with AM/PM *)
minute ::= Integer (* 00-59 *)
second ::= Integer (* 00-59 *)
am_pm ::= "AM" | "PM" (* case-insensitive; switches to 12-hour clock *)
datetime_content ::= date_content ("," )? time_content (* comma or space separated *)
duration_content ::= duration_term+
duration_term ::= Integer duration_unit
duration_unit ::= "y" | "mo" | "w" | "d" | "h" | "min" | "s"
(* Physical-quantity smart literal. The parenthesised content is a value and a
unit expression, parsed by the dolfin-units library. The content must NOT
contain a ")". See the Units & Quantities reference. *)
quantity_literal ::= "quantity" "(" quantity_content ")"
quantity_content ::= simple_quantity (arith_op simple_quantity)* ("as" convert_target)?
simple_quantity ::= Number unit_expr | Number (* bare number = dimensionless *)
arith_op ::= "+" | "-" | "*" | "/"
convert_target ::= unit_expr | "SI"
unit_expr ::= unit_term (("." | "/") unit_term)*
unit_term ::= unit_name ("^" power)?
power ::= Integer | "(" Integer ")" (* e.g. s^(-1) *)
unit_name ::= (* a unit token, optionally SI-prefixed, e.g. m, kg, km, N, km/h components *)
cardinality ::=
"one"
| "any"
| "some"
| "optional"
| Integer
| "at least" Integer
| "at most" Integer
| Integer ".." Integer
| Integer ".." "*"
(* "at least"/"at most" (and the "at_least"/"at_most" underscore aliases) are
shared with the rule quantifiers above. *)