Skip to content

Commit

Permalink
feat(Scope): many operations now take modifiers; modify is removed (#…
Browse files Browse the repository at this point in the history
…108)

* feat(Scope): many operations now take modifiers; `modify` is removed

Optional arguments about contexts are changed as well.

* docs(Scope): unify the wordings of "context of modifier effects"
  • Loading branch information
favonia authored May 21, 2023
1 parent 313f616 commit 8c1ded9
Show file tree
Hide file tree
Showing 4 changed files with 49 additions and 63 deletions.
6 changes: 3 additions & 3 deletions src/ModifierSigs.ml
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ sig
val run : ?not_found:not_found_handler -> ?shadow:shadow_handler -> ?hook:hook_handler -> (unit -> 'a) -> 'a
(** [run f] initializes the engine and runs the thunk [f].
@param not_found [not_found ctx prefix] is called when the engine expects at least one binding within the subtree at [prefix] but could not find any, where [ctx] is the context passed to {!val:S.modify}. Modifiers such as {!val:Language.any}, {!val:Language.only}, {!val:Language.none}, and a few other modifiers expect at least one matching binding. For example, the modifier {!val:Language.except}[ ["x"; "y"]] expects that there was already something under the subtree at [x.y]. If there were actually no names with the prefix [x.y], then the modifier will trigger this effect with [prefix] being [Emp #< "x" #< "y"]. The default handler directly returns the [()].
@param shadow [shadow ctx path x y] is called when item [y] is being assigned to [path] but [x] is already bound at [path], where [ctx] is the context passed to {!val:S.modify}. Modifiers such as {!val:Language.renaming} and {!val:Language.union} could lead to bindings having the same name, and when that happens, this function is called to resolve the conflicting bindings. The default handler directly returns the [y], effectively silencing the effects.
@param hook [hook prefix id input] is called when processing the modifiers created by {!val:Language.hook}, where [ctx] is the context passed to {!val:S.modify}. When the engine encounters the modifier {!val:Language.hook}[ id] while handling the subtree [input] at [prefix], it will call [hook prefix id input] and replace the existing subtree [input] with the return value. The default handler returns [input], effective skipping the hooks. *)
@param not_found [not_found ctx prefix] is called when the engine expects at least one binding within the subtree at [prefix] but could not find any, where [ctx] is the context passed to {!val:modify}. Modifiers such as {!val:Language.any}, {!val:Language.only}, {!val:Language.none}, and a few other modifiers expect at least one matching binding. For example, the modifier {!val:Language.except}[ ["x"; "y"]] expects that there was already something under the subtree at [x.y]. If there were actually no names with the prefix [x.y], then the modifier will trigger this effect with [prefix] being [Emp #< "x" #< "y"]. The default handler directly returns the [()].
@param shadow [shadow ctx path x y] is called when item [y] is being assigned to [path] but [x] is already bound at [path], where [ctx] is the context passed to {!val:modify}. Modifiers such as {!val:Language.renaming} and {!val:Language.union} could lead to bindings having the same name, and when that happens, this function is called to resolve the conflicting bindings. The default handler directly returns the [y], effectively silencing the effects.
@param hook [hook prefix id input] is called when processing the modifiers created by {!val:Language.hook}, where [ctx] is the context passed to {!val:modify}. When the engine encounters the modifier {!val:Language.hook}[ id] while handling the subtree [input] at [prefix], it will call [hook prefix id input] and replace the existing subtree [input] with the return value. The default handler returns [input], effective skipping the hooks. *)

val try_with : ?not_found:not_found_handler -> ?shadow:shadow_handler -> ?hook:hook_handler -> (unit -> 'a) -> 'a
(** [try_with f] runs the thunk [f] and intercepts modifier effects. See the documentation of {!val:run} for the meaning of the optional effect interceptors; the difference is that the default interceptors reperform the intercepted modifier effects instead of silencing them.
Expand Down
38 changes: 18 additions & 20 deletions src/Scope.ml
Original file line number Diff line number Diff line change
Expand Up @@ -42,56 +42,54 @@ struct
M.exclusively @@ fun () ->
Trie.find_singleton p (S.get ()).visible

let modify_visible ?context m =
let modify_visible ?context_visible m =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{s with visible = Mod.modify ?context ~prefix:Emp m s.visible}
{s with visible = Mod.modify ?context:context_visible ~prefix:Emp m s.visible}

let modify_export ?context m =
let modify_export ?context_export m =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{s with export = Mod.modify ?context ~prefix:(export_prefix()) m s.export}
{s with export = Mod.modify ?context:context_export ~prefix:(export_prefix()) m s.export}

let modify_standalone = Mod.modify

let modify = modify_standalone [@@ocaml.alert deprecated "Use modify_standalone"]

let export_visible ?context m =
let export_visible ?context_modifier ?context_export m =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{s with
export =
Trie.union ~prefix:(export_prefix()) (Mod.Perform.shadow context) s.export @@
Mod.modify ?context ~prefix:Emp m s.visible }
Trie.union ~prefix:(export_prefix()) (Mod.Perform.shadow context_export) s.export @@
Mod.modify ?context:context_modifier ~prefix:Emp m s.visible }

let include_singleton ?context_visible ?context_export (path, x) =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{ visible = Trie.union_singleton ~prefix:Emp (Mod.Perform.shadow context_visible) s.visible (path, x);
export = Trie.union_singleton ~prefix:(export_prefix()) (Mod.Perform.shadow context_export) s.export (path, x) }

let import_singleton ?context (path, x) =
let import_singleton ?context_visible (path, x) =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{ s with visible = Trie.union_singleton ~prefix:Emp (Mod.Perform.shadow context) s.visible (path, x) }
{ s with visible = Trie.union_singleton ~prefix:Emp (Mod.Perform.shadow context_visible) s.visible (path, x) }

let unsafe_include_subtree ~context_visible ~context_export (path, ns) =
let unsafe_include_subtree ~context_modifier ~context_visible ~context_export ~modifier (path, ns) =
S.modify @@ fun s ->
let ns = Mod.modify ?context:context_modifier ~prefix:Emp modifier ns in
{ visible = Trie.union_subtree ~prefix:Emp (Mod.Perform.shadow context_visible) s.visible (path, ns);
export = Trie.union_subtree ~prefix:(export_prefix()) (Mod.Perform.shadow context_export) s.export (path, ns) }

let include_subtree ?context_visible ?context_export (path, ns) =
M.exclusively @@ fun () -> unsafe_include_subtree ~context_visible ~context_export (path, ns)
let include_subtree ?context_modifier ?context_visible ?context_export ?(modifier=Language.id) (path, ns) =
M.exclusively @@ fun () -> unsafe_include_subtree ~context_modifier ~context_visible ~context_export ~modifier (path, ns)

let import_subtree ?context (path, ns) =
let import_subtree ?context_modifier ?context_visible ?(modifier=Language.id) (path, ns) =
M.exclusively @@ fun () -> S.modify @@ fun s ->
{ s with visible = Trie.union_subtree ~prefix:Emp (Mod.Perform.shadow context) s.visible (path, ns) }
let ns = Mod.modify ?context:context_modifier ~prefix:Emp modifier ns in
{ s with visible = Trie.union_subtree ~prefix:Emp (Mod.Perform.shadow context_visible) s.visible (path, ns) }

let get_export () =
M.exclusively @@ fun () -> (S.get()).export

let section ?context_visible ?context_export p f =
let section ?context_modifier ?context_visible ?context_export ?(modifier=Language.id) p f =
M.exclusively @@ fun () ->
let ans, export =
Internal.run ~export_prefix:(export_prefix() <>< p) ~init_visible:(S.get()).visible @@ fun () ->
let ans = f () in ans, get_export ()
in
unsafe_include_subtree ~context_visible ~context_export (p, export);
unsafe_include_subtree ~context_modifier ~context_visible ~context_export ~modifier (p, export);
ans

let run ?not_found ?shadow ?hook ?(export_prefix=Emp) ?(init_visible=Trie.empty) f =
Expand Down
64 changes: 26 additions & 38 deletions src/ScopeSigs.ml
Original file line number Diff line number Diff line change
Expand Up @@ -35,20 +35,22 @@ sig
When implementing an OCaml-like language, this is how one can introduce a top-level definition [let p = x].
@param context_visible The context attached to the modifier effects when manipulating the visible namespace.
@param context_export The context attached to the modifier effects when manipulating the export namespace. *)
@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_visible:context -> ?context_export:context -> Trie.path * (data, tag) Trie.t -> unit
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_visible The context attached to the modifier effects when manipulating the visible namespace.
@param context_export The context attached to the modifier effects when manipulating the export namespace. *)
@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}. *)

val import_singleton : ?context:context -> Trie.path * (data * tag) -> unit
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.
Expand All @@ -60,19 +62,21 @@ sig
(* code for handling the expression [e] *)
]}
@param context The context attached to the modifier effects when manipulating the visible namespace.
@since 4.1.0 *)
@param context_visible The context of modifier effects when merging the subtree into the visible namespace.
@since 5.0.0 *)

val import_subtree : ?context:context -> Trie.path * (data, tag) Trie.t -> unit
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 The context attached to the modifier effects. *)
@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}. *)

val modify_visible : ?context:context -> hook Language.t -> unit
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.
Expand All @@ -87,39 +91,23 @@ sig
modify_visible Language.(union [seq []; only ["M"]])
]}
@param context The context attached to the modifier effects. *)
@param context The context of modifier effects. *)

val modify_export : ?context:context -> hook Language.t -> unit
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 The context attached to the modifier effects. *)
@param context_export The context of modifier effects. *)

val modify_standalone : ?context:context -> ?prefix:Trie.bwd_path -> hook Language.t -> (data, tag) Trie.t -> (data, tag) Trie.t
(** Call the internal modifier engine directly on a standalone namespace. See {!val:Yuujinchou.Modifier.S.modify}.
This feature is useful for massaging a namespace before adding its content into the current scope.
It does not exist in OCaml-like languages. However, in a Haskell-like language, one can implement [import qualified Mod (x, y)] as follows:
{[
let m = modify_standalone Language.(union [only ["x"]; only ["y"]]) m in
import_subtree (["Mod"], m)
]}
The purpose of re-using the internal modifier engine is to share the same effect handling. For example, if one has already used {!val:run} or {!val:try_with} to mute the [Mod.Shadow] effect, it also applies to the call of this [modify]. A new instance of {!module:Yuujinchou.Modifier.Make} will use fresh algebraic effects and thus existing effect handling will not apply.
This function will not lock the current scope, because it will not access the current scope.
@since 4.1.0 *)

val modify : ?context:context -> ?prefix:Trie.bwd_path -> hook Language.t -> (data, tag) Trie.t -> (data, tag) Trie.t
[@@ocaml.alert deprecated "Use modify_standalone"]

val export_visible : ?context:context -> hook Language.t -> unit
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 The context attached to the modifier effects. *)
@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.
Expand All @@ -128,7 +116,7 @@ sig

(** {1 Local Scopes and Sections} *)

val section : ?context_visible:context -> ?context_export:context -> Trie.path -> (unit -> 'a) -> 'a
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
Expand All @@ -146,10 +134,10 @@ section {
} // this section exports y but not x
v}
@param context_visible The context attached to the modifier effects
when merging the content of the section into its parent's visible namespace.
@param context_export The context attached to the modifier effects
when merging the content of the section into its parent's export namespace. *)
@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. *)

(** {1 Runners} *)

Expand Down
4 changes: 2 additions & 2 deletions test/Example.ml
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ let rec interpret_decl : decl -> unit =
S.try_with ~shadow:S.Silence.shadow @@ fun () ->
S.include_singleton (p, (x, `Local))
| Import (t, m) ->
let t = S.modify_standalone m (Trie.Untagged.tag `Imported t) in
S.import_subtree ([], t)
let t = Trie.Untagged.tag `Imported t in
S.import_subtree ~modifier:m ([], t)
| PrintVisible ->
S.modify_visible (Language.hook Print)
| Export p ->
Expand Down

0 comments on commit 8c1ded9

Please sign in to comment.