diff --git a/Docs/01-introduction/key-features.md b/Docs/01-introduction/key-features.md index d545d87e..3bbb8cf8 100644 --- a/Docs/01-introduction/key-features.md +++ b/Docs/01-introduction/key-features.md @@ -150,7 +150,7 @@ Features: - Methods (called "actions") - Inheritance with `extends` - Interfaces with `implements` -- Events and event handlers +- Events that an action can `trigger` (attaching handlers is not a shipped form yet) ## 7. Comprehensive Standard Library diff --git a/Docs/reference/language-specification.md b/Docs/reference/language-specification.md index 383ce885..211f10d7 100644 --- a/Docs/reference/language-specification.md +++ b/Docs/reference/language-specification.md @@ -199,12 +199,20 @@ end try ``` create container [extends ] [implements ]: [property : ]* - [action [with parameters ]: + [action [needs : , ...]: end]* end ``` +**Interface Definition:** +``` +create interface [extends ] +create interface [extends ]: + [requires action [needs : , ...] [: ]]* +end +``` + ### Expressions **Literals:** numbers, text, booleans, lists diff --git a/Docs/reference/reserved-keywords.md b/Docs/reference/reserved-keywords.md index 8d8580fb..f7148f90 100644 --- a/Docs/reference/reserved-keywords.md +++ b/Docs/reference/reserved-keywords.md @@ -148,20 +148,20 @@ These keywords **MUST** always be reserved and **CANNOT** be used as variable na | `catch` | Error handler | `catch when error:` | | `check` | Start conditional | `check if x is greater than 5:` | | `constant` | Define constant property | `constant property max_size as 100` | -| `container` | Define a class/container | `define container called Person:` | +| `container` | Define a class/container | `create container Person:` | | `continue` | Skip to next iteration | `continue` | | `define` | Define action/container | `define action called test:` | | `display` | Output text | `display "Hello World"` | | `each` | For each loop | `for each item in list:` | | `end` | Close block | `end` | | `event` | Define an event | `event click` | -| `extends` | Inheritance | `container Person extends Human:` | +| `extends` | Inheritance | `create container Employee extends Person:` | | `finally` | Always-run cleanup clause on try | `finally:` | | `for` | For loop | `for each x in items:` | | `forever` | Infinite loop | `repeat forever:` | | `from` | Count loop start | `count from 1 to 10:` | | `if` | Conditional | `check if x is 5:` | -| `implements` | Interface implementation | `container Dog implements Animal:` | +| `implements` | Interface implementation | `create container Dog implements Animal:` | | `in` | For each collection | `for each item in list:` | | `interface` | Interface definition | `create interface Runnable:` | | `load` | Load module | `load module math` | @@ -171,7 +171,7 @@ These keywords **MUST** always be reserved and **CANNOT** be used as variable na | `or` | Logical OR | `check if x is 5 or y is 10:` | | `otherwise` | Else clause | `otherwise:` | | `private` | Private visibility | `private property age` | -| `property` | Container property | `property name as "default"` | +| `property` | Container property | `property name: Text` | | `public` | Public visibility | `public property name` | | `push` | Add to list | `push with myList and item` | | `repeat` | Loop construct | `repeat 10 times:` | @@ -579,7 +579,7 @@ Complete reference table of all 181 keywords. | `command` | Other | Process | ❌ | `execute command` | | `connections` | Other | Web/Network | ❌ | `network connections` | | `constant` | Structural | Declaration | ❌ | `constant property` | -| `container` | Structural | OOP | ❌ | `define container` | +| `container` | Structural | OOP | ❌ | `create container` | | `contains` | Contextual | Comparison | ✅ | `list contains item` | | `content` | Other | File I/O | ❌ | `file content` | | `continue` | Structural | Control Flow | ❌ | `continue loop` | diff --git a/History/dev-diary/2026/2026-08-30-container-feature-validation.md b/History/dev-diary/2026/2026-08-30-container-feature-validation.md new file mode 100644 index 00000000..f9e8a2d7 --- /dev/null +++ b/History/dev-diary/2026/2026-08-30-container-feature-validation.md @@ -0,0 +1,73 @@ +# 2026-08-30 — Documented container and interface features, validated + +## What this is + +A pass over every user-facing container/interface claim — the containers +guide, the language spec's `implements` list, keyword examples, and the +key-features container bullet list — against the release binary. + +## What already worked + +The shipped `create container` / `create new` / `object.action()` / +`object.property` surface ran as documented: typed properties, in-action +mutation, parameterized and returning actions, `extends` with overrides, +multi-level inheritance, `requires action` contracts (including parameters +and interface `extends`), marker interfaces, inherited methods satisfying a +contract, static members, property `defaults`, containers as action +parameter types, and a container implementing more than one interface. + +Gated programs that already covered parts of this (`containers_comprehensive.wfl`, +`containers/interface_contracts.wfl`, the four original docs examples, the +interface Rust suite) still pass. + +## What did not match the docs + +**Interface return types were static-only.** +`Docs/04-advanced-features/containers-oop.md` says a required return type is +checked before the program runs. The type checker already reported +`requires action get_area: Number` vs `action get_area: Text`, but the CLI +treats type diagnostics as warnings and the runtime never compared return +types. A container that failed the contract still defined and continued. + +Runtime conformance now stores each method and each `requires action` return +type and rejects a concrete mismatch when the container definition runs — +the same stop as a missing action or a wrong arity. + +**Keyword examples used a grammar that does not parse.** +`define container called Person:` / `store pet as new Animal` was still the +example in the reserved-keyword table and in +`TestPrograms/docs_examples/keyword_reference/containers_examples.wfl` +(CI-SKIP'd). Those now show `create container` / `create new`. + +**Event handlers were listed as a container feature.** +`key-features.md` claimed "Events and event handlers." Declaring `event` and +`trigger` inside a container works; `on :` does not parse +a handler body. The bullet now says handlers are not a shipped form. + +## Coverage added + +- `TestPrograms/containers/documented_features.wfl` — 14 `describe`/`expect` + cases covering the containers-guide surface plus multiple `implements`, + defaults, and static members. +- `TestPrograms/error_examples/interface_return_type.wfl` and + `interface_static_action.wfl` — gated expected-failure programs. +- `tests/interface_contract_test.rs` — + `container_with_incompatible_return_type_fails_at_runtime`. +- Docs examples for inheritance, override, interface `extends`, marker + interfaces, and property access, registered in the docs-examples manifest. + +## TDD evidence + +Red: `wfl TestPrograms/error_examples/interface_return_type.wfl` exited 0 +and printed `unreachable: a return-type mismatch must fail` against the +pre-change release binary; the new Rust test encodes that same program and +requires a nonzero exit naming `get_area` and `return`. + +Green: after the runtime check, that program exits 1 with +`action 'get_area' returns Text but the interface requires Number`, and the +14 asserted documented-feature tests pass. + +Risk class **R3** (backward compatibility / public contract): a program that +claimed `implements` with a concrete return-type mismatch used to run; it +now stops at the container definition, matching the already-documented +static rule. diff --git a/TestPrograms/containers/documented_features.wfl b/TestPrograms/containers/documented_features.wfl new file mode 100644 index 00000000..e70c9cc3 --- /dev/null +++ b/TestPrograms/containers/documented_features.wfl @@ -0,0 +1,418 @@ +// Asserted coverage of every user-facing container/interface feature +// documented in Docs/04-advanced-features/containers-oop.md, plus the +// related claims in the language spec (multiple implements) and keyword +// reference (defaults, static members). + +create container Person: + property name: Text + property age: Number + + action greet: Text + return "Hello, I am " with name + end + + action set_name needs new_name: Text: + change name to new_name + end + + action get_info: Text + return name with " (" with age with ")" + end +end + +create new Person as alice: + name is "Alice" + age is 28 +end + +alice.set_name("Alice Smith") + +create container Book: + property title: Text + property pages: Number + property is_available: Boolean + + action check_out: + change is_available to no + end +end + +create new Book as my_book: + title is "WFL Guide" + pages is 250 + is_available is yes +end + +my_book.check_out() + +create container Calculator: + property value: Number + + action increase needs amount: Number: + change value to value plus amount + end + + action get_value: Number + return value + end +end + +create new Calculator as calc: + value is 0 +end + +calc.increase(10) +calc.increase(5) + +create container Employee extends Person: + property job_title: Text + property salary: Number + + action greet: Text + return "Hello, I am " with name with ", " with job_title + end + + action get_salary: Number + return salary + end +end + +create new Employee as bob: + name is "Bob" + age is 35 + job_title is "Developer" + salary is 75000 +end + +create container Animal: + property name: Text + + action make_sound: Text + return "Some generic sound" + end +end + +create container Dog extends Animal: + action make_sound: Text + return "Woof! I'm " with name + end +end + +create new Dog as buddy: + name is "Buddy" +end + +create container Mammal extends Animal: + property fur_color: Text + + action coat: Text + return name with " has " with fur_color with " fur" + end +end + +create container Retriever extends Mammal: + property breed: Text + + action make_sound: Text + return "The " with breed with " dog barks!" + end +end + +create new Retriever as rex: + name is "Rex" + fur_color is "golden" + breed is "Golden Retriever" +end + +create interface Drawable: + requires action draw + requires action get_area: Number +end + +create container Rectangle implements Drawable: + property width: Number + property height: Number + + action draw: Text + return "Drawing rectangle: " with width with " x " with height + end + + action get_area: Number + return width times height + end +end + +create new Rectangle as rect: + width is 10 + height is 5 +end + +create interface Shape extends Drawable: + requires action describe_shape: Text +end + +create container Square implements Shape: + property side: Number + + action draw: Text + return "Drawing square: " with side + end + + action get_area: Number + return side times side + end + + action describe_shape: Text + return "a square with side " with side + end +end + +create new Square as sq: + side is 4 +end + +create interface Resizable: + requires action resize needs w: Number, h: Number +end + +create container Panel implements Resizable: + property width: Number + property height: Number + + action resize needs w: Number, h: Number: + change width to w + change height to h + end + + action area: Number + return width times height + end +end + +create new Panel as panel: + width is 1 + height is 1 +end + +panel.resize(20, 30) + +create interface Greeter: + requires action greet: Text +end + +create container Staff extends Person implements Greeter: + property job_title: Text +end + +create new Staff as cara: + name is "Cara" + age is 40 + job_title is "Manager" +end + +create interface Serializable + +create container Tagged implements Serializable: + property id: Number + + action ping: Text + return "pong" + end +end + +create new Tagged as thing: + id is 1 +end + +create interface Pingable: + requires action ping: Text +end + +create interface Pongable: + requires action pong: Text +end + +create container Dual implements Pingable, Pongable: + property label: Text + + action ping: Text + return "ping-" with label + end + + action pong: Text + return "pong-" with label + end +end + +create new Dual as dual: + label is "both" +end + +create container Item: + property amount: Number defaults 7 +end + +create new Item as defaulted: +end + +create container Counter: + static property total: Number defaults 41 + + static action answer: Number + return total plus 1 + end + + static action increment: Number + change total to total plus 1 + return total + end +end + +store static_property as Counter.total +store static_answer as Counter.answer() +store static_incremented as Counter.increment() +store static_persisted as Counter.total +display static_property +display static_answer +display static_incremented +display static_persisted + +create container Task: + property description: Text + property completed: Boolean + property priority: Number + + action mark_complete: + change completed to yes + end + + action set_priority needs level: Number: + change priority to level + end + + action to_string: Text + store mark as "open" + check if completed is yes: + change mark to "done" + end check + return mark with " " with description with " P" with priority + end +end + +create container TaskList: + property tasks: List + + action add_task needs task: Task: + push with tasks and task + end + + action complete_first: + check if length of tasks is greater than 0: + store first_task as tasks[0] + first_task.mark_complete() + end check + end + + action first_label: Text + store first_task as tasks[0] + return first_task.to_string() + end +end + +create new Task as task1: + description is "Learn WFL" + completed is no + priority is 1 +end + +create new Task as task2: + description is "Build web server" + completed is no + priority is 2 +end + +task2.set_priority(3) + +create new TaskList as my_tasks: + tasks is [] +end + +my_tasks.add_task(task1) +my_tasks.add_task(task2) +my_tasks.complete_first() + +describe "documented container features": + test "instances keep typed property initializers": + expect alice.age to equal 28 + expect my_book.title to equal "WFL Guide" + expect my_book.pages to equal 250 + end test + + test "actions can read and return properties": + expect alice.get_info() to equal "Alice Smith (28)" + expect alice.greet() to equal "Hello, I am Alice Smith" + end test + + test "actions mutate properties from inside the container": + expect my_book.is_available to equal no + expect calc.get_value() to equal 15 + end test + + test "child containers inherit parent properties and override actions": + expect bob.age to equal 35 + expect bob.get_salary() to equal 75000 + expect bob.greet() to equal "Hello, I am Bob, Developer" + expect buddy.make_sound() to equal "Woof! I'm Buddy" + end test + + test "multi-level inheritance keeps ancestor properties and overrides": + expect rex.coat() to equal "Rex has golden fur" + expect rex.make_sound() to equal "The Golden Retriever dog barks!" + end test + + test "interfaces require actions and return types": + expect rect.draw() to equal "Drawing rectangle: 10 x 5" + expect rect.get_area() to equal 50 + end test + + test "interface inheritance accumulates required actions": + expect sq.draw() to equal "Drawing square: 4" + expect sq.get_area() to equal 16 + expect sq.describe_shape() to equal "a square with side 4" + end test + + test "required actions may declare parameters": + expect panel.area() to equal 600 + end test + + test "inherited actions satisfy interface contracts": + expect cara.greet() to equal "Hello, I am Cara" + end test + + test "marker interfaces accept any implementer": + expect thing.ping() to equal "pong" + expect thing.id to equal 1 + end test + + test "a container may implement more than one interface": + expect dual.ping() to equal "ping-both" + expect dual.pong() to equal "pong-both" + end test + + test "property defaults apply when an initializer is omitted": + expect defaulted.amount to equal 7 + end test + + test "static properties and actions are called on the container name": + expect static_property to equal 41 + expect static_answer to equal 42 + expect static_incremented to equal 42 + expect static_persisted to equal 42 + end test + + test "containers compose: lists of instances and typed parameters": + expect length of my_tasks.tasks to equal 2 + expect my_tasks.first_label() to equal "done Learn WFL P1" + expect task2.to_string() to equal "open Build web server P3" + end test +end describe diff --git a/TestPrograms/docs_examples/_meta/manifest.json b/TestPrograms/docs_examples/_meta/manifest.json index 066e8abc..06dadad0 100644 --- a/TestPrograms/docs_examples/_meta/manifest.json +++ b/TestPrograms/docs_examples/_meta/manifest.json @@ -581,5 +581,97 @@ "complete-example" ], "doc_purpose": "Complete task manager example combining containers, lists, and actions" + }, + "docs_examples/containers/inheritance_01.wfl": { + "doc_section": "Docs/04-advanced-features/containers-oop.md#inheritance", + "type": "executable", + "validate_layers": [ + 1, + 2, + 3, + 4, + 5 + ], + "expected_exit_code": 0, + "tags": [ + "containers", + "oop", + "inheritance" + ], + "doc_purpose": "Demonstrates container inheritance with extends and inherited properties" + }, + "docs_examples/containers/override_01.wfl": { + "doc_section": "Docs/04-advanced-features/containers-oop.md#overriding-actions", + "type": "executable", + "validate_layers": [ + 1, + 2, + 3, + 4, + 5 + ], + "expected_exit_code": 0, + "tags": [ + "containers", + "oop", + "override" + ], + "doc_purpose": "Demonstrates a child container overriding a parent action" + }, + "docs_examples/containers/interface_extends_01.wfl": { + "doc_section": "Docs/04-advanced-features/containers-oop.md#interface-inheritance", + "type": "executable", + "validate_layers": [ + 1, + 2, + 3, + 4, + 5 + ], + "expected_exit_code": 0, + "tags": [ + "containers", + "oop", + "interfaces", + "extends" + ], + "doc_purpose": "Demonstrates interface inheritance accumulating required actions" + }, + "docs_examples/containers/marker_interface_01.wfl": { + "doc_section": "Docs/04-advanced-features/containers-oop.md#marker-interfaces", + "type": "executable", + "validate_layers": [ + 1, + 2, + 3, + 4, + 5 + ], + "expected_exit_code": 0, + "tags": [ + "containers", + "oop", + "interfaces", + "marker" + ], + "doc_purpose": "Demonstrates a bare marker interface as an empty contract" + }, + "docs_examples/containers/property_access_01.wfl": { + "doc_section": "Docs/04-advanced-features/containers-oop.md#accessing-properties", + "type": "executable", + "validate_layers": [ + 1, + 2, + 3, + 4, + 5 + ], + "expected_exit_code": 0, + "tags": [ + "containers", + "oop", + "properties" + ], + "doc_purpose": "Demonstrates reading instance properties with object.property" } } diff --git a/TestPrograms/docs_examples/containers/inheritance_01.wfl b/TestPrograms/docs_examples/containers/inheritance_01.wfl new file mode 100644 index 00000000..76ebbd0f --- /dev/null +++ b/TestPrograms/docs_examples/containers/inheritance_01.wfl @@ -0,0 +1,31 @@ +create container Person: + property name: Text + property age: Number + + action greet: + display "Hello, I am " with name + end +end + +create container Employee extends Person: + property job_title: Text + property salary: Number + + action greet: + display "Hello, I am " with name with ", " with job_title + end + + action get_salary: Number + return salary + end +end + +create new Employee as bob: + name is "Bob" + age is 35 + job_title is "Developer" + salary is 75000 +end + +bob.greet() +display bob.get_salary() diff --git a/TestPrograms/docs_examples/containers/interface_extends_01.wfl b/TestPrograms/docs_examples/containers/interface_extends_01.wfl new file mode 100644 index 00000000..e6653eca --- /dev/null +++ b/TestPrograms/docs_examples/containers/interface_extends_01.wfl @@ -0,0 +1,26 @@ +create interface Drawable: + requires action draw +end + +create interface Shape extends Drawable: + requires action get_area: Number +end + +create container Circle implements Shape: + property radius: Number + + action draw: + display "Drawing circle" + end + + action get_area: Number + return 3 times radius times radius + end +end + +create new Circle as dot: + radius is 2 +end + +dot.draw() +display dot.get_area() diff --git a/TestPrograms/docs_examples/containers/marker_interface_01.wfl b/TestPrograms/docs_examples/containers/marker_interface_01.wfl new file mode 100644 index 00000000..3d2d665e --- /dev/null +++ b/TestPrograms/docs_examples/containers/marker_interface_01.wfl @@ -0,0 +1,15 @@ +create interface Serializable + +create container Note implements Serializable: + property body: Text + + action show: + display body + end +end + +create new Note as memo: + body is "saved" +end + +memo.show() diff --git a/TestPrograms/docs_examples/containers/override_01.wfl b/TestPrograms/docs_examples/containers/override_01.wfl new file mode 100644 index 00000000..1c101caf --- /dev/null +++ b/TestPrograms/docs_examples/containers/override_01.wfl @@ -0,0 +1,19 @@ +create container Animal: + property name: Text + + action make_sound: + display "Some generic sound" + end +end + +create container Dog extends Animal: + action make_sound: + display "Woof! I'm " with name + end +end + +create new Dog as buddy: + name is "Buddy" +end + +buddy.make_sound() diff --git a/TestPrograms/docs_examples/containers/property_access_01.wfl b/TestPrograms/docs_examples/containers/property_access_01.wfl new file mode 100644 index 00000000..a45fef0b --- /dev/null +++ b/TestPrograms/docs_examples/containers/property_access_01.wfl @@ -0,0 +1,12 @@ +create container Book: + property title: Text + property pages: Number +end + +create new Book as my_book: + title is "WFL Guide" + pages is 250 +end + +display my_book.title +display my_book.pages diff --git a/TestPrograms/docs_examples/keyword_reference/containers_examples.wfl b/TestPrograms/docs_examples/keyword_reference/containers_examples.wfl index 735293d2..f764bf3e 100644 --- a/TestPrograms/docs_examples/keyword_reference/containers_examples.wfl +++ b/TestPrograms/docs_examples/keyword_reference/containers_examples.wfl @@ -1,42 +1,39 @@ -// CI-SKIP: pre-existing parse errors (was masked by wfl exiting 0 on parse errors); tracked in issue #555 // Containers & OOP Keywords Examples -// Keywords covered: container, property, extends, new +// Keywords covered: container, property, extends, new, action, implements, interface -// Example 1: Basic container definition display "Container examples" -define container called Animal: - property name as "Unknown" - property age_years as 0 +create container Animal: + property name: Text + property age_years: Number - define action called make_sound: + action make_sound: display "Some sound" - end action -end container + end +end -// Example 2: Create instance -store pet as new Animal -change pet property name to "Buddy" -change pet property age_years to 3 +create new Animal as pet: + name is "Buddy" + age_years is 3 +end -display "Pet name: " with pet property name +display "Pet name: " with pet.name -// Example 3: Another container -define container called Vehicle: - property brand as "Generic" - property year_made as 2020 +create container Vehicle: + property brand: Text + property year_made: Number - define action called get_info: - store info as brand with " " with year_made - return info - end action -end container + action get_info: Text + return brand with " " with year_made + end +end -store car as new Vehicle -change car property brand to "Toyota" -change car property year_made to 2024 +create new Vehicle as car: + brand is "Toyota" + year_made is 2024 +end -store car_info as call car action get_info +store car_info as car.get_info() display "Car: " with car_info display "Container examples complete" diff --git a/TestPrograms/docs_examples/keyword_reference/declaration_examples.wfl b/TestPrograms/docs_examples/keyword_reference/declaration_examples.wfl index f2c49f32..c6e4d7c2 100644 --- a/TestPrograms/docs_examples/keyword_reference/declaration_examples.wfl +++ b/TestPrograms/docs_examples/keyword_reference/declaration_examples.wfl @@ -1,4 +1,4 @@ -// CI-SKIP: pre-existing parse errors (was masked by wfl exiting 0 on parse errors); tracked in issue #555 +// CI-SKIP: remaining list-construction syntax is stale (issue #555); container portion uses current create-container syntax // Declaration Keywords Examples // Keywords covered: store, as, change, define, action, called, with, return, property, container @@ -34,24 +34,24 @@ store result as call add_numbers with 5 and 3 display "5 + 3 = " with result // Example 5: Container definition -define container called Person: - property first_name as "Unknown" - property last_name as "Unknown" - property age_value as 0 +create container Person: + property first_name: Text + property last_name: Text + property age_value: Number - define action called get_full_name: - store full as first_name with " " with last_name - return full - end action -end container + action get_full_name: Text + return first_name with " " with last_name + end +end // Example 6: Create instance and use properties -store alice as new Person -change alice property first_name to "Alice" -change alice property last_name to "Smith" -change alice property age_value to 28 +create new Person as alice: + first_name is "Alice" + last_name is "Smith" + age_value is 28 +end -store full_name as call alice action get_full_name +store full_name as alice.get_full_name() display "Full name: " with full_name // Example 7: Lists and create diff --git a/TestPrograms/error_examples/interface_return_type.wfl b/TestPrograms/error_examples/interface_return_type.wfl new file mode 100644 index 00000000..3e22ae0e --- /dev/null +++ b/TestPrograms/error_examples/interface_return_type.wfl @@ -0,0 +1,14 @@ +// Intentional error: required return type is checked statically / at definition. +create interface Measurable: + requires action get_area: Number +end + +create container Card implements Measurable: + property label: Text + + action get_area: Text + return label + end +end + +display "unreachable: a return-type mismatch must fail" diff --git a/TestPrograms/error_examples/interface_static_action.wfl b/TestPrograms/error_examples/interface_static_action.wfl new file mode 100644 index 00000000..77f5eb7e --- /dev/null +++ b/TestPrograms/error_examples/interface_static_action.wfl @@ -0,0 +1,14 @@ +// Intentional error: a static action must not satisfy requires action. +create interface Drawable: + requires action draw +end + +create container Chart implements Drawable: + property title: Text + + static action draw: + display "static draw" + end +end + +display "unreachable: a static action must not satisfy the contract" diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index e3eb2762..d577b598 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -10063,6 +10063,7 @@ impl Interpreter { name, parameters, body, + return_type, line, column, .. @@ -10071,6 +10072,7 @@ impl Interpreter { let container_method = ContainerMethodValue { name: name.clone(), params: parameters.iter().map(|p| p.name.clone()).collect(), + return_type: return_type.clone(), body: body.clone(), is_static: false, is_public: true, @@ -10103,6 +10105,7 @@ impl Interpreter { name, parameters, body, + return_type, line, column, .. @@ -10113,6 +10116,7 @@ impl Interpreter { ContainerMethodValue { name: name.clone(), params: parameters.iter().map(|p| p.name.clone()).collect(), + return_type: return_type.clone(), body: body.clone(), is_static: true, is_public: true, @@ -10291,6 +10295,7 @@ impl Interpreter { let value_action = value::ActionSignature { name: action.name.clone(), params: action.parameters.iter().map(|p| p.name.clone()).collect(), + return_type: action.return_type.clone(), line: action.line, column: action.column, }; @@ -15458,9 +15463,9 @@ impl Interpreter { // Resolve an instance method's parameter count: the container's own // methods first, then the parent chain (bounded against definition // cycles, which the environment cannot otherwise rule out). - let find_method_param_count = |method_name: &str| -> Option { + let find_method = |method_name: &str| -> Option<(usize, Option)> { if let Some(method) = container_methods.get(method_name) { - return Some(method.params.len()); + return Some((method.params.len(), method.return_type.clone())); } let mut parent_name = container_extends.cloned(); let mut visited_parents = HashSet::new(); @@ -15473,7 +15478,7 @@ impl Interpreter { _ => return None, }; if let Some(method) = parent.methods.get(method_name) { - return Some(method.params.len()); + return Some((method.params.len(), method.return_type.clone())); } parent_name = parent.extends.clone(); } @@ -15522,7 +15527,7 @@ impl Interpreter { } for (action_name, signature) in &interface.required_actions { - match find_method_param_count(action_name) { + match find_method(action_name) { None => { return Err(RuntimeError::new( format!( @@ -15533,7 +15538,7 @@ impl Interpreter { column, )); } - Some(count) if count != signature.params.len() => { + Some((count, _)) if count != signature.params.len() => { return Err(RuntimeError::new( format!( "Container '{container_name}' does not satisfy interface '{}': action '{action_name}' takes {count} parameter(s) but the interface requires {}", @@ -15544,7 +15549,23 @@ impl Interpreter { column, )); } - Some(_) => {} + Some((_, return_type)) => { + if let (Some(required_return), Some(actual_return)) = + (&signature.return_type, &return_type) + && !matches!(required_return, Type::Unknown | Type::Any) + && !matches!(actual_return, Type::Unknown | Type::Any) + && required_return != actual_return + { + return Err(RuntimeError::new( + format!( + "Container '{container_name}' does not satisfy interface '{}': action '{action_name}' returns {actual_return} but the interface requires {required_return}", + interface.name + ), + line, + column, + )); + } + } } } } diff --git a/src/interpreter/value.rs b/src/interpreter/value.rs index 68fb8e73..5d26e4a3 100644 --- a/src/interpreter/value.rs +++ b/src/interpreter/value.rs @@ -1,6 +1,6 @@ use super::environment::Environment; use super::error::RuntimeError; -use crate::parser::ast::Statement; +use crate::parser::ast::{Statement, Type}; use crate::pattern::CompiledPattern; use std::cell::RefCell; use std::collections::{HashMap, HashSet}; @@ -160,6 +160,7 @@ pub struct ContainerInstanceValue { pub struct ContainerMethodValue { pub name: String, pub params: Vec, + pub return_type: Option, pub body: Vec, pub is_static: bool, pub is_public: bool, @@ -198,6 +199,7 @@ pub struct InterfaceDefinitionValue { pub struct ActionSignature { pub name: String, pub params: Vec, + pub return_type: Option, pub line: usize, pub column: usize, } diff --git a/tests/interface_contract_test.rs b/tests/interface_contract_test.rs index 4ab37013..7bffdbcc 100644 --- a/tests/interface_contract_test.rs +++ b/tests/interface_contract_test.rs @@ -471,6 +471,42 @@ end ); } +#[test] +fn container_with_incompatible_return_type_fails_at_runtime() { + let program = r#" +create interface Measurable: + requires action get_area: Number +end + +create container Card implements Measurable: + property label: Text + + action get_area: Text + return label + end +end + +display "should not get here" +"#; + let output = run_wfl_program(program, "iface_return_type"); + assert!( + !output.status.success(), + "a return-type mismatch against the interface must fail, got stdout: {} stderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("get_area") && stderr.contains("return"), + "error should name the mismatched action and return types; got: {stderr}" + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!( + !stdout.contains("should not get here"), + "program must not continue past the unsatisfied contract" + ); +} + #[test] fn typechecker_reports_interface_return_type_mismatch() { use wfl::typechecker::TypeChecker;