package yuujinchou

  1. Overview
  2. Docs

Source file ScopeSigs.ml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
module type Param = ModifierSigs.Param
module type Perform = ModifierSigs.Perform

module type S =
sig
  module Language : LanguageSigs.S

  module Param : Param
  open Param

  (** A scope inherently has two namespaces: a {i visible} namespace that dictates what's visible, and an {i export} namespace recording all the names that will be exported. *)

  (** {1 Types of Effect Handlers} *)

  type not_found_handler = context option -> Trie.bwd_path -> unit
  (** The type of a handler of the {!val:Modifier.S.module-Perform.not_found} effect. *)

  type shadow_handler = context option -> Trie.bwd_path -> data * tag -> data * tag -> data * tag
  (** The type of a handler of the {!val:Modifier.S.module-Perform.shadow} effect. *)

  type hook_handler = context option -> Trie.bwd_path -> hook -> (data, tag) Trie.t -> (data, tag) Trie.t
  (** The type of a handler of the {!val:Modifier.S.module-Perform.hook} effect. *)

  (** {1 Exceptions} *)

  exception Locked
  (** The exception [Locked] is raised when an operation on a scope starts before another operation on the same scope is finished.
      This could happen when the user, for example, calls {!val:modify_visible} and then calls {!val:modify_export} when handling the effects.

      The principle is that one should not access any scope in its intermediate states, including looking up a name via {!val:resolve}.
      Any attempt to do so will raise the exception [Locked]; the exception [Locked] signals a serious programming error.

      Note: {!val:section} only locks the parent scope; the child scope is initially unlocked.
  *)

  (** {1 Name Resolution} *)

  val resolve : Trie.path -> (data * tag) option
  (** [resolve p] looks up the name [p] in the visible namespace and returns the data associated with the binding. *)

  (** {1 Inclusion of New Names}

      Inclusion affects both visible and export namespaces, just like [include] in OCaml. *)

  val include_singleton : ?context_visible:context -> ?context_export:context -> Trie.path * (data * tag) -> unit
  (** [include_singleton (p, x)] adds a new binding to both the visible and export namespaces, where the binding is associating the data [x] to the path [p].
      Conflicting names during the final merge will trigger the effect [shadow].
      [include_singleton (p, x)] is equivalent to [include_subtree Trie.(singleton (p, x))], but potentially more efficient.

      When implementing an OCaml-like language, this is how one can introduce a top-level definition [let p = x].

      @param context_visible The context of modifier effects when merging the subtree into the visible namespace.
      @param context_export The context of modifier effects when merging the subtree into the export namespace. *)

  val include_subtree : ?context_modifier:context -> ?context_visible:context -> ?context_export:context -> ?modifier:hook Language.t -> Trie.path * (data, tag) Trie.t -> unit
  (** [include_subtree (p, ns)] merges the namespace [ns] prefixed with [p] into
      both the visible and export namespaces. Conflicting names during the final merge
      will trigger the effect [shadow].

      This feature is useful for introducing multiple top-level definitions at once.

      @param context_modifier The context of modifier effects when applying [modifier].
      @param context_visible The context of modifier effects when merging the subtree into the visible namespace.
      @param context_export The context of modifier effects when merging the subtree into the export namespace.
      @param modifier The modifier applied to the subtree before importing it. The default value is {!val:Language.id}. *)

  (** {1 Importing of New Names}

      Importing affects only the visible namespace, just like [open] in OCaml. *)

  val import_singleton : ?context_visible:context -> Trie.path * (data * tag) -> unit
  (** [import_singleton (p, x)] adds a new binding to the visible namespace (while keeping the export namespace intact), where the binding is associating the data [x] to the path [p].
      Conflicting names during the final merge will trigger the effect [shadow].
      [import_singleton (p, x)] is equivalent to [import_subtree Trie.(singleton (p, x))], but potentially more efficient.

      When implementing an OCaml-like language, one can implement the local binding [let p = x in e] as follows:
      {[
        section [] @@ fun () ->
        import_singleton (p, x);
        (* code for handling the expression [e] *)
      ]}

      @param context_visible The context of modifier effects when merging the subtree into the visible namespace.
      @since 5.0.0 *)

  val import_subtree : ?context_modifier:context -> ?context_visible:context -> ?modifier:hook Language.t -> Trie.path * (data, tag) Trie.t -> unit
  (** [import_subtree (p, ns)] merges the namespace [ns] prefixed with [p] into
      the visible namespace (while keeping the export namespace intact).
      Conflicting names during the final merge will trigger the effect [Mod.Shadowing].

      When implementing an OCaml-like language, one can import content from other compilation units using [import_subtree].

      @param context_modifier The context of modifier effects when applying [modifier].
      @param context_visible The context of modifier effects when merging the subtree into the visible namespace.
      @param modifier The modifier applied to the subtree before importing it. The default value is {!val:Language.id}. *)

  (** {1 Modifying Namespaces} *)

  val modify_visible : ?context_visible:context -> hook Language.t -> unit
  (** [modify_visible m] modifies the visible namespace by
      running the modifier [m] on it, using the internal modifier engine.

      When implementing an OCaml-like language, one can implement [open M] as follows:
      {[
        modify_visible Language.(union [seq []; only ["M"]])
      ]}

      When implementing an OCaml-like language, one can implement [include M] as follows:
      {[
        export_visible Language.(only ["M"]);
        modify_visible Language.(union [seq []; only ["M"]])
      ]}

      @param context The context of modifier effects. *)

  val modify_export : ?context_export:context -> hook Language.t -> unit
  (** [modify_visible m] modifies the export namespace by
      running the modifier [m] on it, using the internal modifier engine.

      @param context_export The context of modifier effects. *)

  (** {1 Exporting Names} *)

  val export_visible : ?context_modifier:context -> ?context_export:context -> hook Language.t -> unit
  (** [export_visible m] runs the modifier [m] on the visible namespace,
      and then merge the result into the export namespace.
      Conflicting names during the final merge will trigger the effect [Mod.Shadowing].

      This feature is useful for implementing a userspace [export] statement. It does not exist in OCaml-like languages.

      @param context_modifier The context of modifier effects when applying the modifier [m].
      @param context_export The context of modifier effects when merging the subtree into the export namespace. *)

  val get_export : unit -> (data, tag) Trie.t
  (** [get_export ()] returns the export namespace of the current scope.

      This is useful for obtaining all exported content when wrapping up a compilation unit. The {!val:section} function internally calls [get_export] when wrapping up a child scope, but an implementer is expected to call [get_export] for the outermost scope. The outermost scope is special because it is the interface of the entire compilation unit and its ending often triggers special handling code ({i e.g.,} caching of declared names for faster scope checking). *)

  (** {1 Sections} *)

  val section : ?context_modifier:context -> ?context_visible:context -> ?context_export:context -> ?modifier:hook Language.t -> Trie.path -> (unit -> 'a) -> 'a
  (** [section p f] starts a new scope and runs the thunk [f] within the scope.
      The child scope inherits the visible namespace from the parent, and its export namespace
      will be prefixed with [p] and merged into both the visible and export namespaces
      of the parent scope.

      A section is similar to a section in Coq or a module in Agda (but not a module in OCaml). This can be used to implement local bindings as well; a local binding is a private definition in a section. For example, in an OCaml-like languages augmented with sections,
      {v
let y = let x = 1 in x
      v}
      is equivalent to
      {v
section {
  private let x = 1
  let y = x
} // this section exports y but not x
      v}

      @param context_modifier The context of modifier effects when applying [modifier] to the content of the section before the merging.
      @param context_visible The context of modifier effects when merging the content of the section into its parent's visible namespace.
      @param context_export The context of modifier effects when merging the content of the section into its parent's export namespace.
      @param modifier The modifier applied to the content of the section before the merging. The default value is {!Language.id}. *)

  (** {1 Runners} *)

  module type Perform = Perform with module Param := Param
  (** The signature of a module implementing all effect handlers for a lexical scope. *)

  module Perform : Perform
  (** The handlers that (re-)perform effects. *)

  module Silence : Perform
  (** The handlers that silence effects. *)

  val run : ?not_found:not_found_handler -> ?shadow:shadow_handler -> ?hook:hook_handler ->
    ?export_prefix:Trie.bwd_path -> ?init_visible:(data, tag) Trie.t -> (unit -> 'a) -> 'a
  (** [run ~not_found ~shadow ~hook f] initializes a scope and executes the thunk [f], using [h] to handle modifier effects.

      @param not_found See {!val:Yuujinchou.Modifier.S.run} for the explanation of this argument.
      @param shadow See {!val:Yuujinchou.Modifier.S.run} for the explanation of this argument.
      @param hook See {!val:Yuujinchou.Modifier.S.run} for the explanation of this argument.
      @param export_prefix The additional global prefix prepended to the paths reported to effect handlers
      originating from export namespaces. The default is the empty path ([Emp]).
      This does not affect paths originating from visible namespaces.
      @param init_visible The initial visible namespace. The default is the empty namespace. *)

  val try_with : ?not_found:not_found_handler -> ?shadow:shadow_handler -> ?hook:hook_handler -> (unit -> 'a) -> 'a
  (** Execute the code and handles the internal modifier effects.

      [try_with] is intended to be used within {!val:run} to intercept or reperform internal effects,
      while {!val:run} is intended to be at the top-level to set up the environment and handle all
      effects by itself. For example, the following function silences the [Mod.Shadow] effects, but the
      silencing function should be used within the dynamic scope of a {!val:run}.
      See also {!val:Yuujinchou.Modifier.S.try_with}.
      {[
        let silence_shadow f =
          try_with ~shadow:Silence.shadow f
      ]}

      A consequence of the semantic difference between {!val:run} and [try_with] is that
      {!val:run} starts a fresh empty scope while [try_with] stays in the current scope.
  *)
end