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
52 changes: 41 additions & 11 deletions ui/vml_model.v
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,18 @@ fn v_list(items []VValue) VValue {
}
}

fn v_value_from[T](value T) VValue {
$if T is string {
fn v_value_from_tracked[T](value T, ancestors []voidptr) VValue {
$if T.unaliased_typ is voidptr {
return VValue{}
} $else $if T is $pointer {
address := unsafe { voidptr(value) }
if isnil(value) || address in ancestors {
Comment on lines +71 to +72

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish pointee types when detecting cycles

When a pointer targets the first field of an ancestor struct, its address is normally identical to the struct's address, so this address-only check incorrectly treats the interior pointer as a cyclic back-edge. For example, if Counter has count int followed by count_ref &int and count_ref = &counter.count, converting &counter makes count_ref invalid instead of exposing the number; the analogous check in v_schema_from_tracked also rejects app.counter.count_ref during validation. Track the pointee type together with the address (or otherwise distinguish interior pointers from actual back-edges).

Useful? React with 👍 / 👎.

return VValue{}
}
mut next := ancestors.clone()
next << address
return v_value_from_pointee(value, next)
} $else $if T is string {
return v_string(value)
} $else $if T is bool {
return v_bool(value)
Expand All @@ -76,14 +86,14 @@ fn v_value_from[T](value T) VValue {
} $else $if T is $array {
mut items := []VValue{cap: value.len}
for item in value {
items << v_value_from(item)
items << v_value_from_tracked(item, ancestors)
}
return v_list(items)
} $else $if T is $struct {
mut fields := map[string]VValue{}
$for field in T.fields {
$if field.is_pub {
fields[field.name] = v_value_from(value.$(field.name))
fields[field.name] = v_value_from_tracked(value.$(field.name), ancestors)
}
}
return v_object(fields)
Expand All @@ -92,16 +102,36 @@ fn v_value_from[T](value T) VValue {
}
}

fn v_array_element_schema[E](_ []E) VSchema {
fn v_array_element_schema[E](_ []E, ancestors []voidptr, nil_types []string) VSchema {
$if E is $struct {
return v_schema_from(E{})
return v_schema_from_tracked[E](E{}, ancestors, nil_types)
} $else {
return v_schema_from($zero(E))
return v_schema_from_tracked[E]($zero(E), ancestors, nil_types)
}
}

fn v_schema_from[T](value T) VSchema {
$if T is string {
fn v_schema_from_tracked[T](value T, ancestors []voidptr, nil_types []string) VSchema {
$if T.unaliased_typ is voidptr {
return VSchema{}
} $else $if T is $pointer {
if isnil(value) {
// Empty arrays and nil references still expose their declared type.
// Stop a recursive nil type instead of expanding it indefinitely.
if T.name in nil_types {
return VSchema{}
}
mut next_types := nil_types.clone()
next_types << T.name
return v_schema_from_nil_pointee(value, ancestors, next_types)
}
address := unsafe { voidptr(value) }
if address in ancestors {
return VSchema{}
}
mut next := ancestors.clone()
next << address
return v_schema_from_pointee(value, next, nil_types)
} $else $if T is string {
return VSchema{
kind: .string_
}
Expand All @@ -114,7 +144,7 @@ fn v_schema_from[T](value T) VSchema {
kind: .number
}
} $else $if T is $array {
element := v_array_element_schema(value)
element := v_array_element_schema(value, ancestors, nil_types)
return VSchema{
kind: .list
element: &element
Expand All @@ -123,7 +153,7 @@ fn v_schema_from[T](value T) VSchema {
mut fields := map[string]VSchema{}
$for field in T.fields {
$if field.is_pub {
fields[field.name] = v_schema_from(value.$(field.name))
fields[field.name] = v_schema_from_tracked(value.$(field.name), ancestors, nil_types)
}
}
return VSchema{
Expand Down
36 changes: 36 additions & 0 deletions ui/vml_pointer_model.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
module ui2

// Track the current reference path, not a global visited set: two model fields
// may legitimately point at the same object. Back-edges and nil values remain
// invalid VML values and produce the usual source-line property-path error.
fn v_value_from[T](value T) VValue {
return v_value_from_tracked[T](value, []voidptr{})
}

fn v_value_from_pointee[E](value &E, ancestors []voidptr) VValue {
$if E is $pointer {
return v_value_from_tracked[E](E(unsafe { voidptr(*value) }), ancestors)
} $else {
return v_value_from_tracked[E](*value, ancestors)
}
}

fn v_schema_from[T](value T) VSchema {
return v_schema_from_tracked[T](value, []voidptr{}, []string{})
}

fn v_schema_from_pointee[E](value &E, ancestors []voidptr, nil_types []string) VSchema {
$if E is $pointer {
return v_schema_from_tracked[E](E(unsafe { voidptr(*value) }), ancestors, nil_types)
} $else {
return v_schema_from_tracked[E](*value, ancestors, nil_types)
}
}

fn v_schema_from_nil_pointee[E](_ &E, ancestors []voidptr, nil_types []string) VSchema {
$if E is $pointer {
return v_schema_from_tracked[E](E(unsafe { nil }), ancestors, nil_types)
} $else {
return v_schema_from_tracked[E]($zero(E), ancestors, nil_types)
}
}
112 changes: 112 additions & 0 deletions ui/vml_pointer_model_test.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
module ui2

pub struct PointerCounter {
pub mut:
count int
next &PointerCounter = unsafe { nil }
}

pub struct PointerCounterApp {
pub mut:
counter &PointerCounter = unsafe { nil }
other &PointerCounter = unsafe { nil }
items []&PointerCounter
}

pub fn (mut app PointerCounterApp) increment() {
app.counter.count++
}

fn test_pointer_model_reads_live_values_after_an_action() {
counter := &PointerCounter{
count: 3
}
mut app := new_vml_app('Screen {
Label { text: app.counter.count }
Button { on_tap: app.increment() }
}', PointerCounterApp{
counter: counter
}) or { panic(err) }
built := app.build(rect(0, 0, 200, 100)) or { panic(err) }
assert built.children[0].text == '3'
app.handle(built.children[1].action_id) or { panic(err) }
assert counter.count == 4
assert (app.build(rect(0, 0, 200, 100)) or { panic(err) }).children[0].text == '4'
}

fn test_pointer_model_handles_shared_nested_and_cyclic_references() {
mut counter := &PointerCounter{
count: 3
}
counter.next = counter
model := PointerCounterApp{
counter: counter
other: counter
}
root := element_from_vml_model('Label { text: app.counter.count + app.other.count }', model, rect(0,
0, 100, 30)) or { panic(err) }
assert root.text == '6.0'
counter.next = &PointerCounter{
count: 5
}
nested := element_from_vml_model('Label { text: app.counter.next.count }', model, rect(0, 0,
100, 30)) or { panic(err) }
assert nested.text == '5'
}

fn test_pointer_model_nil_path_returns_an_error_without_dereferencing() {
if _ := element_from_vml_model('Label { text: app.counter.count }', PointerCounterApp{}, rect(0,
0, 100, 30))
{
assert false, 'a nil counter must not be dereferenced'
} else {
assert err.msg().contains('app.counter.count')
assert err.msg().contains('line 1')
}
// A nil field that is not used by this template must be harmless.
assert (element_from_vml_model('Label { text: "OK" }', PointerCounterApp{}, rect(0, 0, 100, 30)) or {
panic(err)
}).text == 'OK'
}

fn test_pointer_model_arrays_validate_when_empty_and_render_when_populated() {
source := 'Column { Repeater { model: app.items key: item.count Label { text: item.count } } }'
empty := element_from_vml_model(source, PointerCounterApp{}, rect(0, 0, 100, 100)) or {
panic(err)
}
assert empty.children.len == 0
full := element_from_vml_model(source, PointerCounterApp{ items: [&PointerCounter{ count: 7 }] }, rect(0,
0, 100, 100)) or { panic(err) }
assert full.children[0].text == '7'
if _ := element_from_vml_model(source.replace('text: item.count', 'text: item.typo'), PointerCounterApp{}, rect(0,
0, 100, 100))
{
assert false, 'empty pointer arrays must still validate fields'
} else {
assert err.msg().contains('item.typo')
}
}

fn test_pointer_model_dereferences_scalar_and_multiple_indirections() {
number := 42
pointer := &number
assert v_value_from(pointer).number == 42
assert v_value_from(&pointer).number == 42
assert v_schema_from(&pointer).kind == .number
text := 'Hello'
assert v_value_from(&text).text == 'Hello'
flag := true
assert v_value_from(&flag).bool_
assert v_value_from(unsafe { nil }).kind == .invalid
}

fn test_pointer_model_multiple_nil_indirections_are_safe() {
nil_counter := unsafe { &PointerCounter(nil) }
assert v_value_from(&nil_counter).kind == .invalid
assert v_schema_from(&nil_counter).kind == .object
schema := v_schema_from(&nil_counter)
assert (schema.fields['count'] or { panic('missing count schema') }).kind == .number
nil_double := unsafe { &&PointerCounter(nil) }
assert v_schema_from(nil_double).kind == .object
assert v_value_from(nil_double).kind == .invalid
}
Loading