Skip to content

Commit 2577054

Browse files
authored
feat(dsl): initial_markings accessor, hygiene, caller line, shared decomposer (#87)
## Summary Codex review followups for the DSL PR (#69332aa). Four small, independent fixes: - **`initial_markings/0` accessor** — the `initial_marking/2` macro accumulated into `@cf_initial_markings`, but `Builder` never read it. Now exposed as a zero-arity helper returning `[%ColouredFlow.Enactment.Marking{}]` (Runner seed data; intentionally separate from `cpnet/0`'s static definition). - **Hygiene checks** — `guard`/`action`/`on_markings` previously silently overwrote when called twice in the same scope. Now raise `CompileError` pointing at the second declaration. - **Caller line/file in errors** — `guard`, `action`, `on_markings`, `transition`, `termination`, `function`, `input`, `output`, `place`, `initial_marking` all capture `__CALLER__.{file,line}` and thread it through their `__set_*__!` / `__open_*__!` / `__push_arc__` helpers, so `CompileError`s point at the offending line, not the `defmodule` line. - **Shared type decomposer** — `DSL.Function` no longer maintains its own `decompose_type/1`; reuses `Notation.Colset.__decompose_type__/1` (newly exposed under the `__name__/N` "internal but reusable" convention). `Builder.resolve_descr/2` stays — it does the user-name → primitive descr lookup, which is orthogonal to AST decomposition. Plus minor cleanups along the way: replace `ArgumentError` raises in macro bodies with `CompileError`+file/line, fix the `~MS[1, 2, 3]` typo in `spec.md` and `place.ex` docs (the sigil is space-separated), drop a dead `if scope.markings || true do` in termination close. ## Test plan - [x] new tests for `initial_markings/0` (empty, single, multi-place accumulation) - [x] new tests for duplicate `guard` / `action` / `on_markings` (compile via `Code.compile_string` and assert `error.line` matches the second declaration) - [x] existing function tests still pass after the shared-decomposer refactor - [x] `mix format --check-formatted` - [x] `mix compile --warnings-as-errors` - [x] `mix credo --strict` (0 issues) - [x] `mix dialyzer` (baseline `Total errors: 6, Skipped: 6` unchanged) - [x] `MIX_ENV=test mix test` (63 doctests, 410 tests, 0 failures)
1 parent 9a13613 commit 2577054

11 files changed

Lines changed: 417 additions & 130 deletions

File tree

lib/coloured_flow/dsl/arc.ex

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ defmodule ColouredFlow.DSL.Arc do
2323
end
2424
"""
2525
defmacro input(place, expression_or_opts, opts_or_block \\ []) do
26-
build_arc(place, expression_or_opts, opts_or_block, :p_to_t, "input")
26+
build_arc(place, expression_or_opts, opts_or_block, :p_to_t, "input", __CALLER__)
2727
end
2828

2929
@doc """
@@ -40,31 +40,37 @@ defmodule ColouredFlow.DSL.Arc do
4040
end
4141
"""
4242
defmacro output(place, expression_or_opts, opts_or_block \\ []) do
43-
build_arc(place, expression_or_opts, opts_or_block, :t_to_p, "output")
43+
build_arc(place, expression_or_opts, opts_or_block, :t_to_p, "output", __CALLER__)
4444
end
4545

4646
@spec build_arc(
4747
Macro.t(),
4848
Macro.t(),
4949
Macro.t(),
5050
Arc.orientation(),
51-
String.t()
51+
String.t(),
52+
Macro.Env.t()
5253
) :: Macro.t()
53-
defp build_arc(place, arg2, arg3, orientation, label) do
54-
place_value = unquote_atom!(place, "#{label} place")
55-
{opts, expr_ast} = decompose_args(arg2, arg3, label)
54+
defp build_arc(place, arg2, arg3, orientation, label, caller) do
55+
place_value = unquote_atom!(place, "#{label} place", caller)
56+
{opts, expr_ast} = decompose_args(arg2, arg3, label, caller)
5657

5758
arc_label = Keyword.get(opts, :label)
5859
expression = ExpressionHelper.build_arc_expression!(orientation, expr_ast)
5960

6061
quote do
61-
ColouredFlow.DSL.Transition.__push_arc__(__MODULE__, %Arc{
62-
label: unquote(arc_label),
63-
orientation: unquote(orientation),
64-
transition: nil,
65-
place: unquote(Atom.to_string(place_value)),
66-
expression: unquote(Macro.escape(expression))
67-
})
62+
ColouredFlow.DSL.Transition.__push_arc__(
63+
__MODULE__,
64+
%Arc{
65+
label: unquote(arc_label),
66+
orientation: unquote(orientation),
67+
transition: nil,
68+
place: unquote(Atom.to_string(place_value)),
69+
expression: unquote(Macro.escape(expression))
70+
},
71+
unquote(caller.file),
72+
unquote(caller.line)
73+
)
6874
end
6975
end
7076

@@ -75,10 +81,10 @@ defmodule ColouredFlow.DSL.Arc do
7581
# - (place, expression, opts_keyword) -- arg3 is keyword (no :do)
7682
# - (place, opts_keyword, do: expression) -- arg2 is keyword, arg3 has :do
7783
# - (place, do: expression) -- arg2 is keyword with :do, arg3 == []
78-
@spec decompose_args(Macro.t(), Macro.t(), String.t()) :: {keyword(), Macro.t()}
79-
defp decompose_args(arg2, arg3, label)
84+
@spec decompose_args(Macro.t(), Macro.t(), String.t(), Macro.Env.t()) :: {keyword(), Macro.t()}
85+
defp decompose_args(arg2, arg3, label, caller)
8086

81-
defp decompose_args(arg2, [], _label) do
87+
defp decompose_args(arg2, [], _label, _caller) do
8288
if keyword?(arg2) and Keyword.has_key?(arg2, :do) do
8389
{body, opts} = Keyword.pop!(arg2, :do)
8490
{opts, body}
@@ -87,7 +93,7 @@ defmodule ColouredFlow.DSL.Arc do
8793
end
8894
end
8995

90-
defp decompose_args(arg2, arg3, label) when is_list(arg3) do
96+
defp decompose_args(arg2, arg3, label, caller) when is_list(arg3) do
9197
cond do
9298
Keyword.has_key?(arg3, :do) and keyword?(arg2) ->
9399
{body, opts_after_do} = Keyword.pop!(arg3, :do)
@@ -101,13 +107,18 @@ defmodule ColouredFlow.DSL.Arc do
101107
{arg3, arg2}
102108

103109
true ->
104-
raise ArgumentError, "Invalid `#{label}` arguments: #{inspect(arg3)}"
110+
raise CompileError,
111+
description: "Invalid `#{label}` arguments: #{inspect(arg3)}",
112+
file: caller.file,
113+
line: caller.line
105114
end
106115
end
107116

108-
defp decompose_args(arg2, arg3, label) do
109-
raise ArgumentError,
110-
"Invalid `#{label}` arguments: arg2=#{inspect(arg2)}, arg3=#{inspect(arg3)}"
117+
defp decompose_args(arg2, arg3, label, caller) do
118+
raise CompileError,
119+
description: "Invalid `#{label}` arguments: arg2=#{inspect(arg2)}, arg3=#{inspect(arg3)}",
120+
file: caller.file,
121+
line: caller.line
111122
end
112123

113124
defp keyword?(list) when is_list(list) do
@@ -120,10 +131,13 @@ defmodule ColouredFlow.DSL.Arc do
120131

121132
defp keyword?(_other), do: false
122133

123-
@spec unquote_atom!(Macro.t(), String.t()) :: atom()
124-
defp unquote_atom!(value, _label) when is_atom(value), do: value
134+
@spec unquote_atom!(Macro.t(), String.t(), Macro.Env.t()) :: atom()
135+
defp unquote_atom!(value, _label, _caller) when is_atom(value), do: value
125136

126-
defp unquote_atom!(value, label) do
127-
raise ArgumentError, "Expected #{label} to be an atom, got: #{Macro.to_string(value)}"
137+
defp unquote_atom!(value, label, caller) do
138+
raise CompileError,
139+
description: "Expected #{label} to be an atom, got: #{Macro.to_string(value)}",
140+
file: caller.file,
141+
line: caller.line
128142
end
129143
end

lib/coloured_flow/dsl/builder.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ defmodule ColouredFlow.DSL.Builder do
1010
alias ColouredFlow.Definition.ColouredPetriNet
1111
alias ColouredFlow.Definition.Place
1212
alias ColouredFlow.Definition.Procedure
13+
alias ColouredFlow.Enactment.Marking
1314

1415
@doc false
1516
defmacro __before_compile__(env) do
@@ -20,6 +21,7 @@ defmodule ColouredFlow.DSL.Builder do
2021
# and lets the arc validator allow free vars on outgoing arcs that are
2122
# produced by the action.
2223
cpnet = ColouredFlow.Builder.build(cpnet)
24+
initial_markings = build_initial_markings(env.module)
2325

2426
case ColouredFlow.Validators.run(cpnet) do
2527
{:ok, validated} ->
@@ -31,6 +33,14 @@ defmodule ColouredFlow.DSL.Builder do
3133
@spec cpnet() :: ColouredPetriNet.t()
3234
def cpnet, do: unquote(Macro.escape(validated))
3335

36+
@doc """
37+
The list of `%ColouredFlow.Enactment.Marking{}` structs declared via
38+
`initial_marking/2`. Used by the Runner to seed an enactment; this is *not* part
39+
of the static CPN definition (see `cpnet/0`).
40+
"""
41+
@spec initial_markings() :: [Marking.t()]
42+
def initial_markings, do: unquote(Macro.escape(initial_markings))
43+
3444
@doc """
3545
The human-readable name of this workflow, or `nil` if unset.
3646
"""
@@ -56,6 +66,13 @@ defmodule ColouredFlow.DSL.Builder do
5666
end
5767
end
5868

69+
@spec build_initial_markings(module()) :: [Marking.t()]
70+
defp build_initial_markings(module) do
71+
module
72+
|> pull(:cf_initial_markings)
73+
|> Enum.map(fn {place, tokens} -> %Marking{place: place, tokens: tokens} end)
74+
end
75+
5976
@spec build_cpnet(module()) :: ColouredPetriNet.t()
6077
defp build_cpnet(module) do
6178
%ColouredPetriNet{

lib/coloured_flow/dsl/function.ex

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,9 @@ defmodule ColouredFlow.DSL.Function do
2121
end
2222
"""
2323
defmacro function(head_with_type, body \\ nil) do
24-
{name, args, result} = decompose_head(head_with_type)
24+
caller_file = __CALLER__.file
25+
caller_line = __CALLER__.line
26+
{name, args, result} = decompose_head(head_with_type, __CALLER__)
2527
expr_ast = ExpressionHelper.block_to_ast(body)
2628
expression = ExpressionHelper.build_from_ast!(expr_ast)
2729

@@ -39,7 +41,9 @@ defmodule ColouredFlow.DSL.Function do
3941
unquote(name_ast),
4042
unquote(args_ast),
4143
unquote(missing_ast),
42-
unquote(extra_ast)
44+
unquote(extra_ast),
45+
unquote(caller_file),
46+
unquote(caller_line)
4347
)
4448

4549
@cf_functions %Procedure{
@@ -51,79 +55,59 @@ defmodule ColouredFlow.DSL.Function do
5155
end
5256

5357
@doc false
54-
@spec __validate_args__!(atom(), [atom()], [atom()], [atom()]) :: :ok
55-
def __validate_args__!(name, args, missing, _extra) do
58+
@spec __validate_args__!(atom(), [atom()], [atom()], [atom()], String.t(), non_neg_integer()) ::
59+
:ok
60+
def __validate_args__!(name, args, missing, _extra, file, line) do
5661
if missing != [] do
5762
raise CompileError,
5863
description: """
5964
Function `#{name}/#{length(args)}` declares argument(s) \
6065
#{inspect(missing)}, but they are not referenced in the body.
61-
"""
66+
""",
67+
file: file,
68+
line: line
6269
end
6370

6471
# Extra free variables are allowed (other vars/constants resolved at the
6572
# workflow level), so we don't reject them here.
6673
:ok
6774
end
6875

69-
@spec decompose_head(Macro.t()) :: {atom(), [atom()], ColouredFlow.Definition.ColourSet.descr()}
70-
defp decompose_head({:"::", _meta, [head, type_ast]}) do
71-
{name, args} = decompose_call(head)
72-
type_descr = decompose_type(type_ast)
76+
@spec decompose_head(Macro.t(), Macro.Env.t()) ::
77+
{atom(), [atom()], ColouredFlow.Definition.ColourSet.descr()}
78+
defp decompose_head({:"::", _meta, [head, type_ast]}, caller) do
79+
{name, args} = decompose_call(head, caller)
80+
type_descr = ColouredFlow.Notation.Colset.__decompose_type__(type_ast)
7381
{name, args, type_descr}
7482
end
7583

76-
defp decompose_head(other) do
77-
raise ArgumentError, """
78-
Invalid function head, expected `name(arg1, arg2) :: type()`,
79-
got: #{Macro.to_string(other)}
80-
"""
84+
defp decompose_head(other, caller) do
85+
raise CompileError,
86+
description: """
87+
Invalid function head, expected `name(arg1, arg2) :: type()`,
88+
got: #{Macro.to_string(other)}
89+
""",
90+
file: caller.file,
91+
line: caller.line
8192
end
8293

83-
defp decompose_call({name, _meta, args}) when is_atom(name) and is_list(args) do
94+
defp decompose_call({name, _meta, args}, caller) when is_atom(name) and is_list(args) do
8495
arg_names =
8596
Enum.map(args, fn
8697
{arg, _meta, ctx} when is_atom(arg) and is_atom(ctx) ->
8798
arg
8899

89100
other ->
90-
raise ArgumentError,
91-
"function argument must be a variable, got: #{Macro.to_string(other)}"
101+
raise CompileError,
102+
description: "function argument must be a variable, got: #{Macro.to_string(other)}",
103+
file: caller.file,
104+
line: caller.line
92105
end)
93106

94107
{name, arg_names}
95108
end
96109

97-
defp decompose_call({name, _meta, ctx}) when is_atom(name) and is_atom(ctx) do
110+
defp decompose_call({name, _meta, ctx}, _caller) when is_atom(name) and is_atom(ctx) do
98111
{name, []}
99112
end
100-
101-
# Reuse Notation.Colset's type decomposer through a public-ish path. Falls
102-
# back to a small local impl for the cases we need.
103-
@spec decompose_type(Macro.t()) :: ColouredFlow.Definition.ColourSet.descr()
104-
defp decompose_type({:{}, _meta, []}), do: {:unit, []}
105-
106-
defp decompose_type({type1, type2}) do
107-
{:tuple, [decompose_type(type1), decompose_type(type2)]}
108-
end
109-
110-
defp decompose_type({:{}, _meta, types}) do
111-
{:tuple, Enum.map(types, &decompose_type/1)}
112-
end
113-
114-
defp decompose_type({:%{}, _meta, fields}) do
115-
map = Map.new(fields, fn {key, type} -> {key, decompose_type(type)} end)
116-
{:map, map}
117-
end
118-
119-
defp decompose_type({:list, _meta, [type]}) do
120-
{:list, decompose_type(type)}
121-
end
122-
123-
defp decompose_type(type) do
124-
case Macro.decompose_call(type) do
125-
{name, []} when is_atom(name) -> {name, []}
126-
_other -> raise ArgumentError, "Invalid function return type: #{Macro.to_string(type)}"
127-
end
128-
end
129113
end

lib/coloured_flow/dsl/place.ex

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ defmodule ColouredFlow.DSL.Place do
1515
place :output, :int
1616
"""
1717
defmacro place(name, colour_set) do
18-
name_value = unquote_atom!(name, "place name")
19-
colour_set_value = unquote_atom!(colour_set, "place colour set")
18+
name_value = unquote_atom!(name, "place name", __CALLER__)
19+
colour_set_value = unquote_atom!(colour_set, "place colour set", __CALLER__)
2020

2121
quote do
2222
@cf_places %Place{
@@ -32,20 +32,23 @@ defmodule ColouredFlow.DSL.Place do
3232
3333
## Examples
3434
35-
initial_marking :input, ~MS[1, 2, 3]
35+
initial_marking :input, ~MS[1 2 3]
3636
"""
3737
defmacro initial_marking(name, marking) do
38-
name_value = unquote_atom!(name, "initial_marking place name")
38+
name_value = unquote_atom!(name, "initial_marking place name", __CALLER__)
3939

4040
quote do
4141
@cf_initial_markings {unquote(Atom.to_string(name_value)), unquote(marking)}
4242
end
4343
end
4444

45-
@spec unquote_atom!(Macro.t(), String.t()) :: atom()
46-
defp unquote_atom!(value, _label) when is_atom(value), do: value
45+
@spec unquote_atom!(Macro.t(), String.t(), Macro.Env.t()) :: atom()
46+
defp unquote_atom!(value, _label, _caller) when is_atom(value), do: value
4747

48-
defp unquote_atom!(value, label) do
49-
raise ArgumentError, "Expected #{label} to be an atom, got: #{Macro.to_string(value)}"
48+
defp unquote_atom!(value, label, caller) do
49+
raise CompileError,
50+
description: "Expected #{label} to be an atom, got: #{Macro.to_string(value)}",
51+
file: caller.file,
52+
line: caller.line
5053
end
5154
end

lib/coloured_flow/dsl/spec.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ time.
3232
place :input, :int
3333
place :output, :int
3434

35-
initial_marking :input, ~MS[1, 2, 3]
35+
initial_marking :input, ~MS[1 2 3]
3636

3737
transition :pass_through do
3838
guard x > 0
@@ -70,9 +70,14 @@ declarations and runs `ColouredFlow.Validators.run/1` against it. Any
7070
validator failure raises at compile time so misconfigured workflows never
7171
reach runtime.
7272

73-
Compiled modules expose a single zero-arity helper:
73+
Compiled modules expose two zero-arity helpers:
7474

75-
MyWorkflow.cpnet() :: %ColouredFlow.Definition.ColouredPetriNet{}
75+
MyWorkflow.cpnet() :: %ColouredFlow.Definition.ColouredPetriNet{}
76+
MyWorkflow.initial_markings() :: [%ColouredFlow.Enactment.Marking{}]
77+
78+
`cpnet/0` returns the static CPN definition. `initial_markings/0` returns
79+
the list of `%Marking{}` structs declared via `initial_marking/2` — Runner
80+
seed data, deliberately *not* part of `cpnet/0`.
7681

7782
Higher-level integration (running an enactment, persisting it, etc.) is
7883
intentionally left to the existing `ColouredFlow.Runner.*` API. The DSL
@@ -162,7 +167,7 @@ Declare the initial marking for a place. Multiple `initial_marking/2`
162167
calls may target different places; they are scattered freely between
163168
other declarations.
164169

165-
initial_marking :input, ~MS[1, 2, 3]
170+
initial_marking :input, ~MS[1 2 3]
166171

167172
### `function/2` and `function/3`
168173

0 commit comments

Comments
 (0)