// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
priv struct ValidationCtx {
inherited_global_names : @set.Set[String]
seen_names : @set.Set[String]
seen_long : @set.Set[String]
seen_short : @set.Set[Char]
args : Array[Arg]
}
///|
fn ValidationCtx::new(
inherited_global_names? : @set.Set[String] = Set([]),
) -> ValidationCtx {
{
inherited_global_names: inherited_global_names.copy(),
seen_names: Set([]),
seen_long: Set([]),
seen_short: Set([]),
args: [],
}
}
///|
fn ValidationCtx::record_arg(
self : ValidationCtx,
arg : Arg,
) -> Unit raise ArgBuildError {
if !self.seen_names.add_and_check(arg.name) {
raise Unsupported("duplicate arg name: \{arg.name}")
}
if !arg.global && self.inherited_global_names.contains(arg.name) {
raise Unsupported(
"arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global",
)
}
fn check_long(name) raise _ {
if !self.seen_long.add_and_check(name) {
raise ArgBuildError::Unsupported("duplicate long option: --\{name}")
}
}
fn check_short(short) raise _ {
if !self.seen_short.add_and_check(short) {
raise ArgBuildError::Unsupported("duplicate short option: -\{short}")
}
}
match arg.info {
FlagInfo(long~, short~, negatable~, ..) => {
if long is Some(name) {
check_long(name)
if negatable {
check_long("no-\{name}")
}
}
if short is Some(short) {
check_short(short)
}
}
OptionInfo(long~, short~, ..) => {
if long is Some(name) {
check_long(name)
}
if short is Some(short) {
check_short(short)
}
}
PositionalInfo(_) => ()
}
self.args.push(arg)
}
///|
fn ValidationCtx::finalize(self : ValidationCtx) -> Unit raise ArgBuildError {
validate_requires_conflicts_targets(self.args, self.seen_names)
}
///|
fn validate_command(
cmd : Command,
args : Array[Arg],
groups : Array[ArgGroup],
inherited_globals : Array[Arg],
) -> Unit raise ArgBuildError {
if cmd.build_error is Some(err) {
raise err
}
validate_inherited_global_shadowing(args, inherited_globals)
validate_group_defs(args, groups)
validate_group_refs(args, groups)
validate_subcommand_defs(cmd.subcommands)
validate_subcommand_required_policy(cmd)
validate_default_subcommand_policy(cmd)
validate_help_subcommand(cmd)
validate_version_actions(cmd)
let child_inherited_globals = merge_inherited_globals(
inherited_globals,
collect_globals(args),
)
for sub in cmd.subcommands {
validate_command(sub, sub.args, sub.groups, child_inherited_globals)
}
}
///|
fn validate_inherited_global_shadowing(
args : Array[Arg],
inherited_globals : Array[Arg],
) -> Unit raise ArgBuildError {
for arg in args {
if inherited_globals.iter().find_first(g => g.name == arg.name)
is Some(inherited_arg) {
if !arg.global {
raise Unsupported(
"arg '\{arg.name}' shadows an inherited global; rename the arg or mark it global",
)
}
if !global_override_compatible(inherited_arg, arg) {
raise Unsupported(
"global arg '\{arg.name}' is incompatible with inherited global definition",
)
}
}
match arg.info {
FlagInfo(long~, short~, negatable~, ..) => {
if long is Some(name) {
validate_inherited_global_long_collision(arg, name, inherited_globals)
if negatable {
validate_inherited_global_long_collision(
arg,
"no-\{name}",
inherited_globals,
)
}
}
if short is Some(value) {
validate_inherited_global_short_collision(
arg, value, inherited_globals,
)
}
}
OptionInfo(long~, short~, ..) => {
if long is Some(name) {
validate_inherited_global_long_collision(arg, name, inherited_globals)
}
if short is Some(value) {
validate_inherited_global_short_collision(
arg, value, inherited_globals,
)
}
}
PositionalInfo(_) => ()
}
}
}
///|
fn merge_inherited_globals(
inherited_globals : Array[Arg],
globals_here : Array[Arg],
) -> Array[Arg] {
let merged = inherited_globals.copy()
for global in globals_here {
match merged.search_by(arg => arg.name == global.name) {
Some(idx) => merged[idx] = global
None => merged.push(global)
}
}
merged
}
///|
fn global_override_compatible(inherited_arg : Arg, arg : Arg) -> Bool {
match inherited_arg.info {
FlagInfo(action=inherited_action, negatable=inherited_negatable, ..) =>
match arg.info {
FlagInfo(action~, negatable~, ..) =>
inherited_action == action && inherited_negatable == negatable
_ => false
}
OptionInfo(action=inherited_action, ..) =>
match arg.info {
OptionInfo(action~, ..) => inherited_action == action
_ => false
}
PositionalInfo(_) => false
}
}
///|
fn validate_inherited_global_long_collision(
arg : Arg,
long : String,
inherited_globals : Array[Arg],
) -> Unit raise ArgBuildError {
if inherited_global_long_owner(arg.name, long, inherited_globals)
is Some(owner) {
raise Unsupported(
"arg '\{arg.name}' long option --\{long} conflicts with inherited global '\{owner}'",
)
}
}
///|
fn validate_inherited_global_short_collision(
arg : Arg,
short : Char,
inherited_globals : Array[Arg],
) -> Unit raise ArgBuildError {
if inherited_global_short_owner(arg.name, short, inherited_globals)
is Some(owner) {
raise Unsupported(
"arg '\{arg.name}' short option -\{short} conflicts with inherited global '\{owner}'",
)
}
}
///|
fn inherited_global_long_owner(
current_name : String,
long : String,
inherited_globals : Array[Arg],
) -> String? {
for inherited in inherited_globals {
if inherited.name == current_name {
continue
}
match inherited.info {
FlagInfo(long=inherited_long, negatable~, ..) =>
if inherited_long is Some(name) &&
(name == long || (negatable && "no-\{name}" == long)) {
return Some(inherited.name)
}
OptionInfo(long=inherited_long, ..) =>
if inherited_long is Some(name) && name == long {
return Some(inherited.name)
}
PositionalInfo(_) => ()
}
}
None
}
///|
fn inherited_global_short_owner(
current_name : String,
short : Char,
inherited_globals : Array[Arg],
) -> String? {
for inherited in inherited_globals {
if inherited.name == current_name {
continue
}
match inherited.info {
FlagInfo(short=inherited_short, ..)
| OptionInfo(short=inherited_short, ..) =>
if inherited_short is Some(value) && value == short {
return Some(inherited.name)
}
PositionalInfo(_) => ()
}
}
None
}
///|
fn validate_flag_arg(
arg : Arg,
ctx : ValidationCtx,
) -> Unit raise ArgBuildError {
validate_named_option_arg(arg)
guard! arg.info is FlagInfo(action~, negatable~, ..)
if action is (Help | Version) {
guard !negatable else {
raise Unsupported("help/version actions do not support negatable")
}
guard arg.env is None else {
raise Unsupported("help/version actions do not support env/defaults")
}
guard !arg.multiple else {
raise Unsupported("help/version actions do not support multiple values")
}
}
ctx.record_arg(arg)
}
///|
fn validate_option_arg(
arg : Arg,
ctx : ValidationCtx,
) -> Unit raise ArgBuildError {
validate_named_option_arg(arg)
validate_default_values(arg)
ctx.record_arg(arg)
}
///|
fn validate_positional_arg(
arg : Arg,
ctx : ValidationCtx,
) -> Unit raise ArgBuildError {
let (min, max) = arg_min_max_for_validate(arg)
if (min > 1 || (max is Some(m) && m > 1)) && !arg.multiple {
raise Unsupported(
"multiple values require action=Append or num_args allowing >1",
)
}
validate_default_values(arg)
ctx.record_arg(arg)
}
///|
fn validate_named_option_arg(arg : Arg) -> Unit raise ArgBuildError {
guard! arg.info
is (FlagInfo(long~, short~, ..) | OptionInfo(long~, short~, ..))
guard long is Some(_) || short is Some(_) || arg.env is Some(_) else {
raise Unsupported("flag/option args require short/long/env")
}
}
///|
fn validate_default_values(arg : Arg) -> Unit raise ArgBuildError {
if arg.info
is (OptionInfo(default_values~, ..) | PositionalInfo(default_values~, ..)) &&
default_values is Some(values) &&
values.length() > 1 &&
!arg.multiple &&
!(arg.info is OptionInfo(action=Append, ..)) {
raise Unsupported(
"default_values with multiple entries require action=Append",
)
}
}
///|
fn validate_group_defs(
args : Array[Arg],
groups : Array[ArgGroup],
) -> Unit raise ArgBuildError {
let seen : @set.Set[String] = Set([])
let arg_seen : @set.Set[String] = Set([])
for arg in args {
arg_seen.add(arg.name)
}
for group in groups {
if !seen.add_and_check(group.name) {
raise Unsupported("duplicate group: \{group.name}")
}
}
for group in groups {
for required in group.requires {
if required == group.name {
raise Unsupported("group cannot require itself: \{group.name}")
}
if !seen.contains(required) && !arg_seen.contains(required) {
raise Unsupported(
"unknown group requires target: \{group.name} -> \{required}",
)
}
}
for conflict in group.conflicts_with {
if conflict == group.name {
raise Unsupported("group cannot conflict with itself: \{group.name}")
}
if !seen.contains(conflict) && !arg_seen.contains(conflict) {
raise Unsupported(
"unknown group conflicts_with target: \{group.name} -> \{conflict}",
)
}
}
}
}
///|
fn validate_group_refs(
args : Array[Arg],
groups : Array[ArgGroup],
) -> Unit raise ArgBuildError {
if groups.is_empty() {
return
}
let arg_index : @set.Set[String] = Set([])
for arg in args {
arg_index.add(arg.name)
}
for group in groups {
for name in group.args {
if !arg_index.contains(name) {
raise Unsupported("unknown group arg: \{group.name} -> \{name}")
}
}
}
}
///|
fn validate_requires_conflicts_targets(
args : Array[Arg],
seen_names : @set.Set[String],
) -> Unit raise ArgBuildError {
for arg in args {
for required in arg.requires {
if required == arg.name {
raise Unsupported("arg cannot require itself: \{arg.name}")
}
if !seen_names.contains(required) {
raise Unsupported("unknown requires target: \{arg.name} -> \{required}")
}
}
for conflict in arg.conflicts_with {
if conflict == arg.name {
raise Unsupported("arg cannot conflict with itself: \{arg.name}")
}
if !seen_names.contains(conflict) {
raise Unsupported(
"unknown conflicts_with target: \{arg.name} -> \{conflict}",
)
}
}
}
}
///|
fn validate_subcommand_defs(subs : Array[Command]) -> Unit raise ArgBuildError {
if subs.is_empty() {
return
}
let seen : @set.Set[String] = Set([])
for sub in subs {
if !seen.add_and_check(sub.name) {
raise Unsupported("duplicate subcommand: \{sub.name}")
}
}
}
///|
fn validate_subcommand_required_policy(
cmd : Command,
) -> Unit raise ArgBuildError {
if cmd.subcommand_required && cmd.subcommands.is_empty() {
raise Unsupported("subcommand_required requires at least one subcommand")
}
}
///|
fn validate_default_subcommand_policy(
cmd : Command,
) -> Unit raise ArgBuildError {
guard cmd.default_subcommand is Some(name) else { return }
if cmd.subcommand_required {
raise Unsupported(
"default_subcommand cannot be used with subcommand_required",
)
}
if cmd.arg_required_else_help {
raise Unsupported(
"default_subcommand cannot be used with arg_required_else_help",
)
}
if cmd.subcommands.iter().find_first(sub => sub.name == name && !sub.hidden)
is None {
raise Unsupported(
"default_subcommand must name a visible subcommand: \{name}",
)
}
if !cmd.groups.is_empty() {
raise Unsupported("default_subcommand does not support root groups")
}
for arg in cmd.args {
if !(arg.global && arg.info is (FlagInfo(_) | OptionInfo(_))) {
raise Unsupported(
"default_subcommand only supports global root flags/options",
)
}
}
}
///|
fn validate_help_subcommand(cmd : Command) -> Unit raise ArgBuildError {
if help_subcommand_enabled(cmd) &&
cmd.subcommands.any(cmd => cmd.name == "help") {
raise Unsupported(
"subcommand name reserved for built-in help: help (disable with disable_help_subcommand)",
)
}
}
///|
fn validate_version_actions(cmd : Command) -> Unit raise ArgBuildError {
if cmd.version is None &&
cmd.args.any(arg => arg.info is FlagInfo(action=Version, ..)) {
raise Unsupported("version action requires command version text")
}
}
///|
fn validate_command_policies(
cmd : Command,
matches : Matches,
) -> Unit raise ArgParseError {
if cmd.subcommand_required &&
!cmd.subcommands.is_empty() &&
matches.parsed_subcommand is None {
raise MissingRequired("subcommand", None)
}
}
///|
fn validate_groups(
args : Array[Arg],
groups : Array[ArgGroup],
matches : Matches,
) -> Unit raise ArgParseError {
if groups.is_empty() {
return
}
let group_presence = Map([])
let group_seen : @set.Set[String] = Set([])
let arg_seen : @set.Set[String] = Set([])
for group in groups {
group_seen.add(group.name)
}
for arg in args {
arg_seen.add(arg.name)
}
for group in groups {
let count = for arg in args; count = 0 {
if !arg_in_group(arg, group) {
continue count
}
if matches_has_value_or_flag(matches, arg.name) {
continue count + 1
} else {
continue count
}
} nobreak {
count
}
group_presence[group.name] = count
if group.required && count == 0 {
raise MissingGroup(group.name)
}
if !group.multiple && count > 1 {
raise GroupConflict(group.name)
}
}
for group in groups {
let count = group_presence[group.name]
if count == 0 {
continue
}
for required in group.requires {
if group_seen.contains(required) {
if group_presence.get(required).unwrap_or(0) == 0 {
raise MissingGroup(required)
}
} else if arg_seen.contains(required) {
if !matches_has_value_or_flag(matches, required) {
raise MissingRequired(required, None)
}
}
}
for conflict in group.conflicts_with {
if group_seen.contains(conflict) {
if group_presence.get(conflict).unwrap_or(0) > 0 {
raise GroupConflict("\{group.name} conflicts with \{conflict}")
}
} else if arg_seen.contains(conflict) {
if matches_has_value_or_flag(matches, conflict) {
raise GroupConflict("\{group.name} conflicts with \{conflict}")
}
}
}
}
}
///|
fn arg_in_group(arg : Arg, group : ArgGroup) -> Bool {
group.args.contains(arg.name)
}
///|
fn validate_values(
args : Array[Arg],
matches : Matches,
) -> Unit raise ArgParseError {
for arg in args {
let present = matches_has_value_or_flag(matches, arg.name)
if arg.required && !present {
raise MissingRequired(arg.name, None)
}
guard arg.info is (OptionInfo(_) | PositionalInfo(_)) else { continue }
if !present {
if arg.info is PositionalInfo(_) {
let (min, _) = arg_min_max(arg)
if min > 0 {
raise TooFewValues(arg.name, 0, min)
}
}
continue
}
let values = matches.values.get(arg.name).unwrap_or([])
let count = values.length()
let (min, max) = arg_min_max(arg)
if count < min {
raise TooFewValues(arg.name, count, min)
}
if !(arg.info is OptionInfo(action=Append, ..)) {
match max {
Some(max) if count > max => raise TooManyValues(arg.name, count, max)
_ => ()
}
}
}
}
///|
fn validate_relationships(
matches : Matches,
args : Array[Arg],
) -> Unit raise ArgParseError {
for arg in args {
if !matches_has_value_or_flag(matches, arg.name) {
continue
}
for required in arg.requires {
if !matches_has_value_or_flag(matches, required) {
raise MissingRequired(required, Some(arg.name))
}
}
for conflict in arg.conflicts_with {
if matches_has_value_or_flag(matches, conflict) {
raise InvalidArgument(
"conflicting arguments: \{arg.name} and \{conflict}",
)
}
}
}
}