Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 7 additions & 32 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 3 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ repository = "https://github.com/wrightkit/wright"
# parser, emitter, detection, validation, and Workshop IR. This is the single
# released reference for the cutover — workspace crates consume it via
# `workshop-rs.workspace = true`.
workshop-rs = "=0.1.11"
opy-rs = "=0.1.5"
opy-compiler = "=0.1.5"
del-rs = { package = "deltin-rs", version = "=0.1.1" }
workshop-rs = "=0.1.18"
opy-rs = "=0.1.18"
del-rs = { package = "deltin-rs", version = "=0.1.2" }
libc = "0.2"
serde = "1"
serde_json = "1"
Expand Down
2 changes: 0 additions & 2 deletions compatibility/ostw/reconstruction/support-boundary.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,6 @@
},
"rejected": [
{ "kind": "forPlayerVariable", "code": "reconstruct-unsupported-action", "reason": "The per-player loop form has no OSTW source form on the declared surface (the frontend lowers loop counters as globals)." },
{ "kind": "debug", "code": "reconstruct-unsupported-action", "reason": "Wright's dedicated debug action has no OSTW source binding." },
{ "kind": "print", "code": "reconstruct-unsupported-action", "reason": "Wright's dedicated print action has no OSTW source binding." },
{ "kind": "settings", "code": "reconstruct-unsupported-program-settings", "reason": "Custom-game settings (Program.settings) have no OSTW source form." },
{ "kind": "unboundAction", "code": "reconstruct-unbound-call", "reason": "An action call whose catalog id has no OSTW source binding (e.g. createBeamEffect) cannot be emitted." },
{ "kind": "unboundValue", "code": "reconstruct-unbound-call", "reason": "A value call whose catalog id has no OSTW source binding (e.g. getHealth) cannot be emitted." },
Expand Down
122 changes: 108 additions & 14 deletions crates/wright-analyzer/src/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,7 +412,11 @@ impl Analysis for RepeatedValue {
"this value expression is evaluated {} times within the same loop scope",
family.len()
),
span: program.values.get(first).and_then(|node| node.span),
span: program
.values
.get(first)
.and_then(|node| node.span)
.or_else(|| program.actions.get(action_id).and_then(Action::span)),
rule,
action: Some(action_id),
value: Some(first),
Expand Down Expand Up @@ -570,10 +574,7 @@ fn visit_action_value_roots(
out: &mut Vec<ValueId>,
) {
match action {
Action::SetGlobalVariable { value, .. }
| Action::ModifyGlobalVariable { value, .. }
| Action::Debug { value, .. }
| Action::Print { message: value, .. } => {
Action::SetGlobalVariable { value, .. } | Action::ModifyGlobalVariable { value, .. } => {
visit_value_with_parent(program, *value, parents, out);
}
Action::SetPlayerVariable { player, value, .. }
Expand All @@ -596,6 +597,13 @@ fn visit_action_value_roots(
| Action::ForPlayerVariable { .. } => {
// Nested loops are excluded from the enclosing loop's scope.
}
Action::Call { name, args, .. }
if name == "createHudText" && is_wright_hud_text_marker(program, args) =>
{
if let Some(value) = synthetic_hud_text_source_value(program, args) {
visit_value_with_parent(program, value, parents, out);
}
}
Action::Call { args, .. } => {
for arg in args {
visit_value_with_parent(program, *arg, parents, out);
Expand All @@ -604,6 +612,89 @@ fn visit_action_value_roots(
}
}

fn is_wright_hud_text_marker(program: &wir::Program, args: &[ValueId]) -> bool {
// workshop-rs 0.1.18 has no action metadata field. The exact fixed
// canonical shape below is therefore the marker carried by Wright's
// debug/print lowering; ordinary createHudText calls keep full traversal.
let [
all_players,
header,
_body,
subheader,
position,
sort_order,
header_color,
subheader_color,
text_color,
reevaluation,
visibility,
] = args
else {
return false;
};
let is_all_players = matches!(
program.values.get(*all_players).map(|node| &node.value),
Some(Value::Call { name, args })
if name == "allPlayers"
&& args.len() == 1
&& value_is_enum(program, args[0], "Team", "ALL")
);
is_all_players
&& value_is_null(program, *header)
&& value_is_null(program, *subheader)
&& value_is_enum(program, *position, "HudPosition", "LEFT")
&& value_is_number(program, *sort_order, -9999.0)
&& value_is_enum(program, *header_color, "Color", "WHITE")
&& value_is_enum(program, *subheader_color, "Color", "WHITE")
&& value_is_enum(program, *text_color, "Color", "WHITE")
&& value_is_enum(program, *reevaluation, "HudReeval", "VISIBILITY_AND_STRING")
&& value_is_enum(program, *visibility, "SpecVisibility", "DEFAULT")
}

fn synthetic_hud_text_source_value(program: &wir::Program, args: &[ValueId]) -> Option<ValueId> {
let value = if args.get(1).is_some_and(|id| {
matches!(
program.values.get(*id).map(|node| &node.value),
Some(Value::Null)
)
}) {
args.get(2)
} else {
args.get(1)
}?;
if let Some(Value::Call { name, args }) = program.values.get(*value).map(|node| &node.value)
&& name == "customString"
{
args.get(1).copied().or(Some(*value))
} else {
Some(*value)
}
}

fn value_is_null(program: &wir::Program, id: ValueId) -> bool {
matches!(
program.values.get(id).map(|node| &node.value),
Some(Value::Null)
)
}

fn value_is_enum(program: &wir::Program, id: ValueId, value_type: &str, value: &str) -> bool {
matches!(
program.values.get(id).map(|node| &node.value),
Some(Value::Enum {
value_type: actual_type,
value: actual_value,
}) if actual_type == value_type && actual_value == value
)
}

fn value_is_number(program: &wir::Program, id: ValueId, expected: f64) -> bool {
matches!(
program.values.get(id).map(|node| &node.value),
Some(Value::Number { value, .. }) if *value == expected
)
}

/// Collect a value and every value in its subtree into `out` (pre-order),
/// recording each child's parent in `parents` for ancestry tests.
fn visit_value_with_parent(
Expand Down Expand Up @@ -854,8 +945,8 @@ fn subtree_has_unprovable_loop(program: &wir::Program, actions: &[ActionId]) ->
/// `If`/`While`/`ForGlobalVariable` subtrees containing any of the above, and
/// a generic `Call` whose name matches a user-defined subroutine (some
/// frontends lower `def`-defined subroutine calls as generic calls rather
/// than `CallSubroutine`) all count as writers. `Debug`, `Print`, and generic
/// `Action::Call`s that are not user subroutines are documented NON-writers:
/// than `CallSubroutine`) all count as writers. Generic `Action::Call`s that
/// are not user subroutines are documented NON-writers:
/// within the supported OPY/Workshop surface (docs/opy/support-matrix.md)
/// user-variable writes lower only to `Set`/`Modify` actions (`.append`
/// lowers to a `Modify` on the variable, so it is caught by the modify
Expand Down Expand Up @@ -909,7 +1000,6 @@ fn action_writes(program: &wir::Program, action: &Action, variable: &Variable) -
| Action::ForPlayerVariable { body, .. } => {
body.iter().any(|id| subtree_writes(program, *id, variable))
}
Action::Debug { .. } | Action::Print { .. } => false,
Action::AssignMember { .. } => true,
}
}
Expand Down Expand Up @@ -1084,8 +1174,6 @@ fn visit_actions(
| Action::SetPlayerVariable { .. }
| Action::ModifyPlayerVariable { .. }
| Action::CallSubroutine { .. }
| Action::Debug { .. }
| Action::Print { .. }
| Action::AssignMember { .. }
| Action::Call { .. } => {}
}
Expand All @@ -1095,10 +1183,9 @@ fn visit_actions(
/// Visit every value reachable from an action's arguments and conditions.
fn visit_values_in_action(program: &wir::Program, action: &Action, f: &mut impl FnMut(ValueId)) {
match action {
Action::SetGlobalVariable { value, .. }
| Action::ModifyGlobalVariable { value, .. }
| Action::Debug { value, .. }
| Action::Print { message: value, .. } => visit_value(program, *value, f),
Action::SetGlobalVariable { value, .. } | Action::ModifyGlobalVariable { value, .. } => {
visit_value(program, *value, f)
}
Action::SetPlayerVariable { player, value, .. }
| Action::ModifyPlayerVariable { player, value, .. } => {
visit_value(program, *player, f);
Expand All @@ -1125,6 +1212,13 @@ fn visit_values_in_action(program: &wir::Program, action: &Action, f: &mut impl
visit_value(program, *stop, f);
visit_value(program, *step, f);
}
Action::Call { name, args, .. }
if name == "createHudText" && is_wright_hud_text_marker(program, args) =>
{
if let Some(value) = synthetic_hud_text_source_value(program, args) {
visit_value(program, value, f);
}
}
Action::Call { args, .. } => {
for arg in args {
visit_value(program, *arg, f);
Expand Down
2 changes: 0 additions & 2 deletions crates/wright-analyzer/src/cfg.rs
Original file line number Diff line number Diff line change
Expand Up @@ -447,8 +447,6 @@ fn action_name(program: &wir::Program, action: ActionId) -> String {
.map_or_else(|| "<dangling>".to_string(), |s| s.name.clone());
format!("callSubroutine {name}")
}
Some(Action::Debug { .. }) => "debug".to_string(),
Some(Action::Print { .. }) => "print".to_string(),
Some(Action::SetGlobalVariable { variable, .. }) => {
let name = program
.global_variables
Expand Down
2 changes: 0 additions & 2 deletions crates/wright-analyzer/src/symbols.rs
Original file line number Diff line number Diff line change
Expand Up @@ -471,8 +471,6 @@ impl<'a> Builder<'a> {
}
Ok(())
}
Action::Debug { value, .. } => self.walk_value(*value, rule, Some(action_id)),
Action::Print { message, .. } => self.walk_value(*message, rule, Some(action_id)),
Action::Call { args, .. } => {
for arg in args {
self.walk_value(*arg, rule, Some(action_id))?;
Expand Down
Loading
Loading