diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index edb5478..a707503 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -15,6 +15,18 @@ jobs: run: make - name: Run tests run: ./claro test + - name: Run current release validation + run: ./claro validate + - name: Validate typecheck diagnostics + run: python3 tools/validate_typecheck_diagnostics.py + - name: Validate version convention + run: python3 tools/validate_version_convention.py + - name: Validate package security + run: python3 tools/validate_package_security.py + - name: Validate compiler warnings + run: python3 tools/validate_compiler_warnings.py + - name: Validate CI workflow coverage + run: python3 tools/validate_ci_workflow.py - name: Check lessons run: | ./claro check lessons/01_hello.claro diff --git a/CHANGELOG.md b/CHANGELOG.md index c925bb3..3103d92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,254 @@ # Changelog +### Diagnose missing class field names + +- `claro typecheck` now explains how to repair a bare `HAS` declaration instead of reporting a confusing missing type for an unnamed field. +- Added focused coverage to the complete typecheck diagnostic validation matrix. + +### Diagnose missing method names + +- `claro typecheck` now explains how to repair a bare `TEACH` declaration inside a class instead of describing it as a top-level function. +- Added focused coverage to the complete typecheck diagnostic validation matrix. + +### Diagnose missing class names + +- `claro typecheck` now explains how to repair a bare `CLASS` declaration instead of silently accepting a class with no name. +- Added focused coverage to the complete typecheck diagnostic validation matrix. + +### Diagnose missing function names + +- `claro typecheck` now explains how to repair a bare `TEACH` declaration instead of silently accepting a function with no name. +- Added focused coverage to the complete typecheck diagnostic validation matrix. + +### Diagnose unknown class field types + +- `claro typecheck` now rejects class fields such as `HAS score BANANA` with a beginner-facing diagnostic that names the class and field, lists supported type examples, and suggests using a Claro type. + +### Validate compatibility duplicate-parameter diagnostics + +- Added focused coverage proving that compatibility `TAKES` / `LEARNED` functions reject repeated parameter names with the same repair-oriented diagnostic as modern functions. +- Added the fixture to the complete typecheck diagnostic validation matrix. + +### Validate compatibility unknown inline method-field annotations + +- Added focused coverage proving that compatibility `TAKES` / `LEARNED` methods reject unknown inline field annotations with the same repair-oriented diagnostic as modern methods. + +### Diagnose unknown inline method-field annotations + +- `claro typecheck` now rejects unknown inline field annotations such as `BANANA` and suggests the class-declared `HAS` type. +- Added focused negative coverage to the complete typecheck validation matrix. + +### Align inline method-field annotations with class fields + +- `claro typecheck` now compares an inline method assignment annotation with the class field declared by `HAS`. +- A mismatch such as `SET score TEXT 10` inside a method for `HAS score NUMBER` explains both types and suggests the class-declared type. +- Added positive and negative fixtures to the complete typecheck validation matrix. + +### Validate compatibility inline method-field annotations + +- Added matching positive and negative coverage for `TEACH ... TAKES ...` / `LEARNED` methods. +- Release validation now protects the same class-declared-type diagnostic for older compatibility lessons. + +- Added focused modern-method coverage for the learner-facing diagnostic produced when an undeclared field is assigned from an expression whose type is not inferable yet. + +## Unreleased + +- Method-body `CHECK TYPE` diagnostics now name the current class method when a declared field is checked against the wrong type, with focused positive and negative validation coverage. + +- Typecheck diagnostics for typed method-body field assignments now name the class method and arithmetic operation when a known TEXT operand is used in a NUMBER field update. + +### Compatibility method field-assignment diagnostics + +- Added focused positive and negative typecheck fixtures for older `TEACH ... TAKES ...` / `LEARNED` methods that assign to typed class fields. +- Release validation now preserves the learner-facing `Class.method` name in these compatibility diagnostics. + +### Validate compatibility method multiplication diagnostics + +- Added focused negative typecheck coverage proving that the older `TAKES` / `LEARNED` method syntax identifies a TEXT operand in a NUMBER multiplication return and explains the required operand type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate compatibility method division diagnostics + +- Added focused negative typecheck coverage proving that the older `TAKES` / `LEARNED` method syntax identifies a TEXT operand in a NUMBER division return and explains the required operand type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate compatibility method subtraction diagnostics + +- Added focused negative typecheck coverage proving that the older `TAKES` / `LEARNED` method syntax identifies a TEXT operand in a NUMBER subtraction return and explains the required operand type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate compatibility method addition diagnostics + +- Added focused negative typecheck coverage proving that the older `TAKES` / `LEARNED` method syntax identifies a TEXT operand in a NUMBER addition return and explains the required operand type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate compatibility method addition returns + +- Added focused positive typecheck coverage proving that the older `TEACH ... TAKES ... RETURNS NUMBER` / `LEARNED` method syntax accepts a numeric addition return after `CHECK TYPE` establishes the parameter type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate compatibility method multiplication returns + +- Added focused release validation for compatibility (`TAKES` / `LEARNED`) methods returning numeric multiplication results. + +### Validate compatibility method subtraction returns + +- Added focused positive typecheck coverage proving that the older `TEACH ... TAKES ... RETURNS NUMBER` / `LEARNED` method syntax accepts a numeric subtraction return after `CHECK TYPE` establishes the parameter type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate numeric function subtraction returns + +- Added focused positive typecheck coverage proving that a typed function can return a numeric subtraction expression after `CHECK TYPE` establishes its parameter type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Validate division return diagnostics + +- Added focused `claro typecheck` coverage proving that a typed function returning `amount / "oops"` identifies division, the required `NUMBER` operands, and the offending `TEXT` value. +- Wired the negative fixture into the complete typecheck validator and `claro validate` coverage. + +### Validate numeric function return expressions + +- Added a focused positive typecheck fixture proving that a typed function can return a numeric multiplication expression after `CHECK TYPE` establishes its parameter type. +- Wired the fixture into the standalone typecheck validator and `claro validate`. + +### Advertise typed-language keywords to IDE helpers + +- Added `RETURNS`, `CHECK`, and `TYPE` to `claro ide` keyword metadata so editor integrations can recognize the current typed function and type-checking syntax. +- Extended the IDE metadata validator and documented the updated completion surface. + +### Validate the complete typecheck matrix + +- `claro validate` now runs the complete focused typecheck fixture matrix, including method-call, alias, return, and expression fixtures that were previously covered only by the standalone diagnostic validator. + +### Validate compatibility method return types + +- Added focused coverage proving that the older `TEACH ... TAKES ... RETURNS TYPE` / `LEARNED` method syntax receives the same learner-facing return-type diagnostic as modern methods. +- Added the compatibility method fixture to the complete typecheck diagnostics validator. + +### Validate incomplete method return paths + +- Added focused typecheck coverage proving that a typed method with an incomplete conditional return reports the same learner-facing missing-path diagnostic as a typed function. +- Added the method fixture to both `claro validate` and the complete typecheck diagnostics validator. + +### Validate compatibility function return types + +- Added focused coverage proving that the older `TEACH ... TAKES ... RETURNS TYPE` / `LEARNED` syntax receives the same learner-facing return-type diagnostic as modern functions. +- Added the compatibility fixture to both `claro validate` and the complete typecheck diagnostics validator. + +### Validate unknown method return types + +- Added focused `claro typecheck` regression coverage for an object method declaring an unsupported `RETURNS` type such as `BANANA`. +- The release validator now checks function and method unknown-return-type diagnostics separately. + +### Diagnose unknown declared return types + +- `claro typecheck` now reports a learner-facing error when a function or object method declares an unsupported `RETURNS` type such as `BANANA`. +- Added focused negative-fixture coverage and release-validator coverage for the invalid declaration. + +### Diagnose empty typed returns + +- `claro typecheck` now reports a beginner-facing error when a function or method declares `RETURNS TYPE` but uses `RETURN` without a value. +- Added a focused negative fixture and release-validation coverage for the missing return expression. + +### Check typed parameter expressions in returns + +- `claro typecheck` now carries a function or method parameter's `CHECK TYPE` metadata into later return-expression inference. +- Added positive and negative arithmetic return fixtures for functions and object methods so a declared return type cannot silently accept an expression whose parameter type is already known. + +### Check simple function return types + +- Added `TEACH name ... RETURNS TYPE` syntax for a focused static return check. +- `claro typecheck` now reports the expected and inferred types when a `RETURN` expression does not match. +- Added positive and negative fixtures and wired both into `claro validate` and the complete typecheck diagnostics validator. + +### Diagnose explicitly typed unknown object fields + +- Added focused typecheck coverage for `SET player.level NUMBER 3` when `Player` declares no `level` field. +- `claro typecheck` now gives the same beginner-facing missing-`HAS` hint for explicitly typed unknown fields as it does for untyped field assignments. + +### Reject unlisted typecheck fixtures + +- `tools/validate_typecheck_diagnostics.py` now discovers `tests/typecheck_*.claro` files and fails when a fixture is missing from the validator's positive or negative lists. +- This keeps new learner-facing typecheck examples from silently escaping release validation. + +### Explain text operands in object-field addition + +- Added focused typecheck coverage for `SET player.score player.name + 1`. +- `claro typecheck` now names the `TEXT` operand and explains that addition into a `NUMBER` field needs numeric values, matching the existing subtraction, multiplication, and division diagnostics. + +### Complete numeric object-field arithmetic coverage + +- Added a focused positive typecheck fixture for numeric subtraction in an object field. +- Added the fixture to both the complete typecheck diagnostics validator and `claro validate`, keeping valid subtraction beside the existing division/multiplication positives and text-operand diagnostics. + +### Complete typecheck fixture validation + +- Added regression coverage requiring the focused typecheck validator to list every `typecheck_*.claro` fixture, plus the existing object-field validation script. +- Added the two previously omitted positive fixtures so `tools/validate_typecheck_diagnostics.py` now exercises the complete typecheck fixture set. + +### CI release-gate validator hardening + +- Added focused regression coverage for the workflow validator. +- `tools/validate_ci_workflow.py` now checks release commands inside actual `run` steps, so commented-out commands cannot make CI coverage appear complete. +- The validator now requires its own executable CI command, protecting the workflow coverage check from being removed silently. + +### Object-alias field CHECK TYPE diagnostics + +- Added focused typecheck coverage for `CHECK TYPE alias.field IS TYPE` after a simple object alias. +- `claro typecheck` now compares an aliased object's declared field type with the requested `CHECK TYPE` type. + +### Nested container type validation + +- Added a focused nested-container typecheck example for adding a typed map to a typed list. +- Updated container matching so `LIST OF MAP` accepts map values with their own type metadata while preserving existing list/map mismatch diagnostics. + +### Object-field expression validation + +- Added positive and negative typecheck fixtures for assigning object fields from another field and from an arithmetic field expression. +- Added both fixtures to `claro validate` and the focused typecheck diagnostics validator. +- Added a focused negative fixture for the reverse expression mismatch: storing a numeric expression in a `TEXT` object field now remains covered by the release validation matrix. + +- `claro package doctor` now rejects duplicate `manifest-version:` fields in `claro.project` instead of accepting the first value and overlooking ambiguous project metadata. + +- `claro package doctor` now rejects duplicate package entries in `claro.lock` instead of allowing ambiguous lockfile data. +- `claro package doctor` now rejects duplicate `name:` entries in a package manifest instead of accepting ambiguous package identity data. +- `claro package doctor` now rejects duplicate `checksum:` entries in a package manifest instead of accepting ambiguous integrity data. +- `claro package doctor` now rejects listed package manifests whose `version:` only starts with the supported local package version, such as `version: 10`. +- `claro package doctor` now rejects listed package manifests whose `source:` only starts with the supported local source, such as `source: local-extra`. +- `claro package doctor` now rejects project files whose `manifest-version:` only starts with the supported value, such as `manifest-version: 10`. + +## v1.18.26-dev exact package manifest-version validation + +- Added package-security coverage for a manifest version that only starts with the supported version, such as `manifest-version: 10` for the current `manifest-version: 1` format. +- Changed `claro package doctor` to compare the complete `manifest-version:` value instead of accepting a version as a substring. + +## v1.18.26-dev exact package checksum validation + +- Added package-security coverage for a manifest checksum with valid digest text followed by extra data. +- Changed `claro package doctor` to compare the complete `checksum:` field instead of accepting a checksum as a substring. + +## v1.18.26-dev exact package manifest-name validation + +- Added package-security coverage for a manifest name that only starts with the expected package name, such as `name: math-tools-extra` for package `math-tools`. +- Changed `claro package doctor` to compare the complete `name:` value, preventing prefix matches from being accepted as valid manifests. + +## v1.18.26-dev package list unsafe-name diagnostic + +- Added focused package-security validation for `claro package list` when `claro.project` already contains an unsafe package name such as `../bad`. +- Changed `claro package list` to fail with the existing beginner-facing `BAD package name: ...` diagnostic instead of displaying unsafe entries as normal package names. + +## v1.18.26-dev missing unchecked function argument diagnostic + +- Added a focused negative `claro typecheck` fixture for calling a simple function without its declared argument, such as `TEACH greet name` followed by `DO greet`. +- Improved the static arity check so simple functions without `CHECK TYPE` hints still report a beginner-facing missing-argument diagnostic. +- Wired the fixture into the typecheck diagnostics validator and `claro validate`. + +## v1.18.26-dev CI release validation coverage + +- Added `tools/validate_ci_workflow.py` to assert that Forgejo/Gitea CI keeps running Claro's current release gates. +- Updated `.forgejo/workflows/ci.yml` to run `claro validate`, typecheck diagnostics validation, version convention validation, and the CI workflow coverage check in addition to the existing smoke tests. +- Documented the CI coverage validator in the testing docs and current status/roadmap notes. + ## v1.18.26-dev unknown object-field expression diagnostic - Added a focused negative `claro typecheck` fixture for assigning an expression with no inferable static type to an undeclared object field, such as `SET player.level score + 1` after `NEW Player player`. diff --git a/CODEBERG_PUSH_HANDOFF.md b/CODEBERG_PUSH_HANDOFF.md index 4f32fe2..0b2a7cb 100644 --- a/CODEBERG_PUSH_HANDOFF.md +++ b/CODEBERG_PUSH_HANDOFF.md @@ -1,33 +1,10 @@ -# Codeberg Push Handoff +# Gitea Push Handoff -Prepared local commit could not be pushed non-interactively. +No prepared local commit is waiting on authentication for the configured private Gitea remote. -- Updated: `2026-08-17T03:01:05Z` -- Local commit: `1cbdc1e56a9509355643cbc7a079f8f6eed442a3` -- Commit message: `feat: diagnose unknown object methods` -- Intended branch: `origin/main` -- Remote head observed before latest auth check: `40f7881afd766d82d2ccb40fa801d3bd9358c7c2` -- Remote head observed after latest auth check: `40f7881afd766d82d2ccb40fa801d3bd9358c7c2` -- Blocker: `CODEBERG_TOKEN` is present, but a read-only Codeberg API identity check returned `HTTP Error 401: Unauthorized`; the token is invalid/expired or lacks access. Because API authentication itself failed, HTTPS Git push was not retried in this run. The token value was not printed or stored. The local branch remains one commit ahead of `origin/main`. +- Updated: `2026-09-01T15:24:30Z` +- Configured publication remote: `gitea` (`ssh://git@gitea.142-44-162-98.sslip.io:2222/RayPals/Claro.git`) +- Latest completed work: `typecheck: validate missing method args` +- Verification rule: compare `git rev-parse HEAD` with `GIT_TERMINAL_PROMPT=0 git ls-remote gitea refs/heads/main` after each push. -Fresh validation in this checkout before the latest push attempt: - -```bash -gcc -std=c99 src/claro.c -O0 -o claro -lm -gcc -std=c99 src/claro.c -O0 -o claro.exe -lm -./claro typecheck tests/typecheck_method_unknown_method_bad.claro # expected nonzero diagnostic asserted -python3 tools/validate_typecheck_diagnostics.py -./claro test -./claro validate -``` - -Latest auth / push verification command: - -```bash -python3 -c '' -GIT_TERMINAL_PROMPT=0 git -c credential.helper= ls-remote origin refs/heads/main -``` - -Result: API authentication failed with `HTTP Error 401: Unauthorized`; push was not retried; remote `main` remained at `40f7881afd766d82d2ccb40fa801d3bd9358c7c2`, not the local commit above. - -After writing or updating this handoff note, rerun validation before making more source changes or retrying the push. +The older Codeberg authentication blocker is historical only. This scheduled job is now configured to publish only to the private Gitea remote. Verify each completed run by comparing local `HEAD` with Gitea `main`. diff --git a/Makefile b/Makefile index b164e24..9ae61a0 100644 --- a/Makefile +++ b/Makefile @@ -2,11 +2,14 @@ CC = gcc CFLAGS = -std=c99 -O0 LDFLAGS = -lm -all: claro +all: claro claro.exe claro: src/claro.c $(CC) $(CFLAGS) -o claro src/claro.c $(LDFLAGS) +claro.exe: src/claro.c + $(CC) $(CFLAGS) -o claro.exe src/claro.c $(LDFLAGS) + test: claro ./claro test diff --git a/docs/ADVANCED_STATIC_TYPING.md b/docs/ADVANCED_STATIC_TYPING.md index d7e183e..c71a36f 100644 --- a/docs/ADVANCED_STATIC_TYPING.md +++ b/docs/ADVANCED_STATIC_TYPING.md @@ -2,6 +2,26 @@ Claro v1.18.26 adds the first advanced static-typing foundation: typed containers checked by `claro typecheck`. +## Complete `TYPE OF` statements + +`TYPE OF` needs an expression, `AS`, and a result name. If the expression is missing, `claro typecheck` gives a repair example instead of silently creating the result variable: + +```claro +TYPE OF AS kind +``` + +```text +TYPE OF needs an expression before AS. Try: TYPE OF score AS kind. +``` + +Add the value whose type should be stored, such as `TYPE OF score AS kind`. + +Class fields need a type after the field name. If a learner writes `HAS score` instead of `HAS score NUMBER`, `claro typecheck` explains the missing piece: + +```text +Class Player field score is missing a type. Add a type such as NUMBER, TEXT, YESNO, LIST, or MAP after the field name. +``` + ## Typed lists ```claro @@ -43,6 +63,472 @@ Output: Type mismatch for map scores: expected NUMBER value, but this value looks like TEXT. ``` +## Nested container values + +A typed list can contain a typed map. `claro typecheck` keeps the map's own type metadata while checking the outer list: + +```claro +SET people AS LIST OF MAP TO LIST +SET person AS MAP OF TEXT TO MAP +PUT person KEY "name" VALUE "Ada" +ADD person TO people +``` + +This is accepted with `Type check OK`. More complex nested container operations and full key/value inference remain future work. + +## Unknown fields inside methods + +When a method assigns a known value to a bare name that is not a parameter, local variable, or declared class field, `claro typecheck` reports the missing field instead of silently treating the typo as a new variable: + +```claro +CLASS Player + HAS score NUMBER + + TEACH train + SET level 3 + END +END +``` + +The diagnostic is: + +```text +Object Player has no field level. Check the field name or add HAS level NUMBER to the class. +``` + +This narrow check covers direct method assignments with a known expression in both modern `TEACH ... END` and compatibility `TEACH ... TAKES ... LEARNED` methods. The focused validation matrix keeps matching positive NUMBER, TEXT, and YESNO assignments in both syntax generations, including `SET ready value` followed by `DO player.toggle YES`. Broader local-variable and control-flow analysis remains future work. + +## Compatibility conditional returns + +The same every-path return check applies to older `TAKES` / `LEARNED` functions. If one branch returns but the other branch does not, `claro typecheck` explains how to repair both paths: + +```claro +TEACH choose TAKES flag RETURNS NUMBER + IF flag + RETURN 1 + END +LEARNED +``` + +```text +Function choose declares RETURNS NUMBER but does not return a NUMBER value on every path. Add RETURN to each branch or after the conditional. +``` + +Use `ELSE` with a `RETURN` value, or add a return after the conditional. This compatibility example is part of the focused release validation matrix. + +A complete compatibility conditional is accepted when both branches return the declared type: + +```claro +TEACH choose TAKES flag RETURNS NUMBER + IF flag + RETURN 1 + ELSE + RETURN 2 + END +LEARNED +``` + +This positive `TAKES` / `LEARNED` example is checked by both the focused typecheck validator and `claro validate`. + +## Complete conditional returns + +### Return expressions that need more information + +When a typed function or method returns a name or expression whose type the focused checker cannot infer yet, `claro typecheck` reports that clearly instead of silently accepting it: + +```claro +TEACH square amount RETURNS NUMBER + RETURN missing +END +``` + +```text +Function square's return value could not be understood yet. Use a NUMBER expression after RETURN. +``` + +Use a known typed value, such as a parameter checked with `CHECK TYPE`, while broader expression inference remains future work. + +The same learner-facing diagnostic applies to compatibility functions written with `TAKES` / `LEARNED`, so older lessons do not get a less-helpful error when a return expression is still unknown. + +Known NUMBER parameters can also be used in arithmetic return expressions. Numeric division is accepted when both operands are known numbers: + +```claro +TEACH halve amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 +END +``` + +This valid division case is included in the focused typecheck validation matrix beside the diagnostic for using a TEXT operand in division. + +Subtraction follows the same rule: + +```claro +TEACH difference amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 2 +END +``` + +The focused validator checks this positive function case as well as the matching subtraction diagnostic for a TEXT operand. + +The same protection applies to object methods. A checked NUMBER parameter can be divided by a number and returned as NUMBER: + +```claro +CLASS Player + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 + END +END +``` + +This valid method-division case is included in the focused typecheck validation matrix beside the method diagnostic for using a TEXT operand in division. + +Return declarations accept the same case-insensitive keyword style as the rest of Claro. For example, `returns NUMBER` still enables return checking: + +```claro +TEACH square amount returns NUMBER + RETURN "oops" +END +``` + +This reports a normal return mismatch naming `NUMBER` and `TEXT`; capitalization style does not disable the safety check. + +The same case-insensitive return declaration rule applies to compatibility methods. A lowercase `returns` still checks the method's `RETURN` value: + +```claro +CLASS Player + TEACH score TAKES amount returns NUMBER + RETURN "oops" + LEARNED +END +``` + +`claro typecheck` reports: + +```text +Type mismatch for return from Player.score: expected NUMBER, but this value looks like TEXT. +``` + +A correct compatibility call also remains valid when the declaration uses lowercase `returns`: + +```claro +NEW Player player +CALL player.score WITH 4 +``` + +This success path is included in the focused typecheck validation matrix. + +Compatibility methods also keep arithmetic return checks when they use the older +`TAKES` / `LEARNED` spelling. For example, this checked division remains a valid +NUMBER return: + +```claro +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 + LEARNED +ENDCLASS +``` + +This matching `CALL player.score WITH 4` path is included in focused validation. + +The same compatibility method return check explains an addition mistake instead of +falling back to a generic return error: + +```claro +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + "oops" + LEARNED +ENDCLASS +``` + +`claro typecheck` reports: + +```text +Type mismatch for return from Player.total: addition needs NUMBER values, but "oops" looks like TEXT. +``` + +This negative compatibility fixture is part of focused release validation. + +Compatibility methods support numeric subtraction in the same focused way: + +```claro +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 2 + LEARNED +ENDCLASS +``` + +The matching `CALL player.score WITH 4` success path is included in focused validation alongside the modern method subtraction case. + +Compatibility methods also keep numeric multiplication return checks. This older +`TAKES` / `LEARNED` spelling accepts a checked NUMBER parameter multiplied by a +number and returned as NUMBER: + +```claro +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * 2 + LEARNED +ENDCLASS +``` + +The matching `CALL player.score WITH 4` success path is included in focused validation. + +Compatibility method division mistakes receive the same operation-specific guidance: + +```claro +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / "oops" + LEARNED +ENDCLASS +``` + +`claro typecheck` reports: + +```text +Type mismatch for return from Player.total: division needs NUMBER values, but "oops" looks like TEXT. +``` + +This negative compatibility fixture is part of focused release validation. + +When a function declares a return type, `claro typecheck` accepts nested conditionals when every branch returns a value of that type: + +```claro +TEACH choose flag RETURNS NUMBER + IF flag + IF flag + RETURN 1 + ELSE + RETURN 2 + END + ELSE + RETURN 3 + END +END +``` + +An incomplete branch still produces a missing-return diagnostic. Deeper control-flow analysis through loops and more complex conditions remains planned. + +## Object aliases and field checks + +When a simple object is assigned to another variable, `claro typecheck` preserves its class for direct field assignments: + +```claro +NEW Player player +SET alias player +SET alias.score 10 +``` + +If the value has the wrong type, the diagnostic names the alias and the expected class field type: + +```text +Type mismatch for field alias.score: expected NUMBER, but this value looks like TEXT. +``` + +This is intentionally limited to simple aliases. Broader object-flow analysis through branches, loops, and complex expressions remains planned. + +The same field-assignment check follows a second simple alias. This lets a learner use a more descriptive name without losing the class field type: + +```claro +SET backup alias +SET backup.score "ten" +``` + +Output: + +```text +Type mismatch for field backup.score: expected NUMBER, but this value looks like TEXT. +``` + +This remains limited to direct assignments through simple alias chains; control-flow and complex object-flow analysis are still planned. + +Text operands are also identified when they appear in arithmetic expressions assigned to NUMBER fields. The diagnostic names the operator and the text operand, which catches a common beginner mistake such as adding a text field to a number field: + +```claro +SET player.score player.name + 1 +``` + +```text +Type mismatch for field player.score: addition needs NUMBER values, but player.name looks like TEXT. +``` + +Subtraction, multiplication, and division use the same beginner-facing guidance: + +```claro +SET player.score player.score - player.name +``` + +```text +Type mismatch for field player.score: subtraction needs NUMBER values, but player.name looks like TEXT. +``` + +The same learner-facing result is preserved for multiplication, so the checker does not lose the useful `TEXT` type when the invalid expression uses `*`: + +```claro +SET player.score player.score * player.name +``` + +```text +Type mismatch for field player.score: multiplication needs NUMBER values, but player.name looks like TEXT. +``` + +Division is covered in the same way: + +```claro +SET player.score player.score / player.name +``` + +```text +Type mismatch for field player.score: division needs NUMBER values, but player.name looks like TEXT. +``` + +Numeric division remains accepted in a NUMBER field, so the type checker protects both sides of this operator without rejecting a valid beginner expression: + +```claro +SET player.score 10 +SET player.score player.score / 2 +``` + +This example is included in the focused typecheck validation matrix. + +Numeric multiplication is accepted in a NUMBER field as well: + +```claro +SET player.score player.score * 2 +``` + +The focused validation matrix keeps this valid arithmetic case beside the multiplication text-operand diagnostic, so a useful expression is not confused with the nearby beginner mistake. + +The expression checker now gives operator-specific guidance for known TEXT operands in subtraction, multiplication, and division, including typed method return expressions. Broader expression inference remains future work. + +Compatibility methods use the same arithmetic return checks as modern methods. This older spelling is accepted when a checked NUMBER parameter is used in a numeric addition: + +```claro +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 2 + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 +``` + +The focused validation matrix covers this positive compatibility example alongside subtraction, multiplication, and division. + +The same operator-specific diagnostic is covered for older compatibility methods: + +```claro +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - "oops" + LEARNED +ENDCLASS +``` + +Claro reports: + +```text +Type mismatch for return from Player.total: subtraction needs NUMBER values, but "oops" looks like TEXT. +``` + +This keeps `TAKES` / `LEARNED` lessons aligned with the modern method syntax. + +Text concatenation is also accepted when the result is assigned to a TEXT field. This keeps a common beginner pattern type-safe without treating every `+` expression as numeric: + +```claro +SET player.name player.name + " Lovelace" +``` + +The focused typecheck validation matrix covers this positive case beside the existing numeric-expression and text-operand checks. + +The same rule works when the literal comes first: + +```claro +SET player.name "Ada " + player.name +``` + +This reverse-order concatenation is also covered by focused validation, so learners can build text from either side without losing the declared `TEXT` field type. + +Compatibility methods use the same TEXT concatenation rule. The older `TAKES` / `LEARNED` spelling accepts both operand orders when a method updates declared `TEXT` fields: + +```claro +TEACH full_name TAKES suffix + SET first first + suffix + SET last "Dr. " + last +LEARNED +``` + +The focused validation matrix covers this compatibility example too, keeping modern and older method lessons aligned. + +Two TEXT fields can be combined as well: + +```claro +SET player.name player.name + player.nickname +``` + +The focused validation matrix covers this field-to-field form so a learner can build text from named object fields without losing the declared `TEXT` type. + +`CHECK TYPE` also preserves TEXT field metadata through the same chained aliases: + +```claro +CLASS Player + HAS name TEXT +END + +NEW Player player +SET alias player +SET backup alias +SET player.name "Ada" +CHECK TYPE backup.name IS TEXT +``` + +This confirms the declared field type without requiring the learner to repeat the original object variable name. The broader limits are unchanged: aliases created through branches, loops, or complex expressions are still planned. + +`CHECK TYPE` also follows simple aliases for direct field metadata checks. This includes a second alias, so learners can give an object a more descriptive local name without losing the class field information: + +```claro +NEW Player player +SET alias player +SET alias.score 10 +CHECK TYPE alias.score IS NUMBER +``` + +```claro +SET backup alias +CHECK TYPE backup.score IS NUMBER +``` + +If the requested type is wrong, the diagnostic names the alias and the declared field type: + +```text +Type check failed: expected TEXT, but alias.score looks like NUMBER. +``` + +The same diagnostic remains specific after the second alias: + +```claro +SET backup alias +CHECK TYPE backup.score IS TEXT +``` + +```text +Type check failed: expected TEXT, but backup.score looks like NUMBER. +``` + ## Function parameter checks Claro now has a small static-checking foundation for function arguments. Keep the beginner-friendly function syntax, then put the expected type inside the function with `CHECK TYPE`: @@ -73,6 +559,23 @@ Output: Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT. ``` +If the learner forgets a checked argument, `claro typecheck` now names the missing parameter and expected type: + +```claro +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square +``` + +Output: + +```text +Function square needs argument amount as NUMBER, but this call does not provide it. +``` + Multiple checked parameters are reported separately, so a learner can fix each argument one at a time: ```claro @@ -93,8 +596,92 @@ Type mismatch for function label: parameter name needs TEXT, but this argument l Type mismatch for function label: parameter age needs NUMBER, but this argument looks like TEXT. ``` +If a learner mistypes a function name, `claro typecheck` now names the unknown function and suggests either checking the spelling or adding a matching `TEACH` block. This is covered for both modern `DO` and older compatibility `CALL ... WITH` calls: + +```claro +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO squre 4 +CALL squre WITH 4 +``` + +Each form reports the same beginner-facing fix: + +```text +Function squre is not known yet. Check the function name or add TEACH squre before calling it. +``` + +## Function and method return checks + +Simple functions and object methods can declare a return type with `RETURNS TYPE`. `claro typecheck` checks each simple `RETURN` expression before the program runs. `RETURN` accepts one expression; if a simple value is followed by extra text, the checker gives a repair hint: + +```text +Function square has extra text after return value amount. Keep only one expression after RETURN. +``` + +```claro +CLASS Player + TEACH score amount RETURNS NUMBER + RETURN amount + END +END + +NEW Player player +DO player.score 4 +``` + +Returning text from this method produces a learner-facing diagnostic that names the class and method: + +```text +Type mismatch for return from Player.score: expected NUMBER, but this value looks like TEXT. +``` + +If a method's return expression is not understood yet, the diagnostic names the method and gives the promised type so the learner knows what to replace: + +```claro +CLASS Player + TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN missing + END +END +``` + +```text +Method Player.square's return value could not be understood yet. Use a NUMBER expression after RETURN. +``` + +Methods can also return a typed field declared by their class. A field is available by its simple name inside the method, so `RETURN score` is checked against `HAS score NUMBER` instead of being treated as an unknown expression. Returning that field from a method declared `RETURNS TEXT` reports the same expected-versus-found diagnostic. + +This is a focused check for simple return expressions. When a parameter has a `CHECK TYPE` declaration, that known type also informs arithmetic return expressions in functions and methods, so `RETURN amount + 1` is checked against the declared return type. The `RETURNS` keyword is case-insensitive in both modern and compatibility function forms, so `returns NUMBER` keeps working while learners are still getting used to Claro's capitalization. A declared return with no `RETURN`, or with a `RETURN` only inside an `IF`, gets a beginner-facing message explaining that the function may finish without returning the promised type: + +```text +Function choose declares RETURNS NUMBER but does not return a NUMBER value on every path. Add RETURN to each branch or after the conditional. +``` + +The focused validation matrix covers modern and compatibility syntax, including a compatibility method that returns a class-declared field, method success and mismatch fixtures, unknown method return expressions, typed field returns, missing returns, and conditional-only returns. It also includes a positive nested `IF`/`ELSE` return example inside a method, so a complete nested method path is protected from regression. Full path-sensitive analysis proving that every `IF`/`ELSE` branch returns remains planned. + ## Object method parameter checks +Method declarations also reject repeated parameter names before calls are checked. This protects both the modern `TEACH ... END` form and the compatibility `TAKES` / `LEARNED` form: + +```claro +CLASS Player + TEACH greet name, name + SAY name + END +END +``` + +```text +Method Player.greet declares parameter name more than once. Give each parameter a different name. +``` + +Give every method parameter a different name. The focused validation matrix covers both method spellings. + Claro also checks simple object method arguments when the method body names a parameter with `CHECK TYPE`. This keeps the method syntax beginner-readable while giving a clearer error before the program runs: ```claro @@ -123,9 +710,33 @@ Output: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT. ``` -Both sides of this narrow method foundation are covered by validation: `tests/typecheck_method_good.claro` checks that `DO player.add 5` is accepted, and `tests/typecheck_method_bad.claro` checks the friendly wrong-type diagnostic. +If the learner forgets a checked method argument, `claro typecheck` also names the missing method parameter and expected type: -If the method call comes before the object is created, the type checker now gives the learner the missing setup step: +```claro +DO player.add +``` + +Output: + +```text +Method Player.add needs argument points as NUMBER, but this call does not provide it. +``` + +If the learner gives too many arguments to a simple checked method, `claro typecheck` now points to the extra argument instead of silently accepting it: + +```claro +DO player.add 5, 6 +``` + +Output: + +```text +Method Player.add only accepts 1 argument, but this call gives 2. Remove the extra argument. +``` + +Both sides of this narrow method foundation are covered by validation: `tests/typecheck_method_good.claro` checks that `DO player.add 5` is accepted, `tests/typecheck_method_bad.claro` checks the friendly wrong-type diagnostic, `tests/typecheck_method_missing_arg_bad.claro` checks the missing checked-argument diagnostic, and `tests/typecheck_method_extra_arg_bad.claro` checks the extra-argument diagnostic. Multi-method class examples are covered too, including the extra-argument case where the checked method appears after another method in the same class. A compatibility `CALL second.add WITH 5, 6` through a two-step alias is also covered by `tests/typecheck_object_alias_method_call_extra_arg_bad.claro`, preserving the same actionable extra-argument message for older lessons. + +If the method call comes before the object is created, the type checker now gives the learner the missing setup step. The modern `DO` form and the older compatibility `CALL ... WITH` form both get this guidance, even when the method name is also a typo: ```claro CLASS Player @@ -138,18 +749,22 @@ CLASS Player END DO player.add 5 +CALL player.add WITH 5 +CALL player.fly WITH 5 ``` Output: ```text Object player is not known yet. Create it with NEW ClassName player before calling player.add. +Object player is not known yet. Create it with NEW ClassName player before calling player.fly. ``` -If the object exists but the class does not declare the method, Claro now names the class and suggests where to add the missing `TEACH` block: +If the object exists but the class does not declare the method, Claro now names the class and suggests where to add the missing `TEACH` block. This is validated for the modern `DO player.fly 5` form and the older compatibility `CALL player.fly WITH 5` form: ```claro DO player.fly 5 +CALL player.fly WITH 5 ``` Output: @@ -158,6 +773,14 @@ Output: Object Player has no method fly. Check the method name or add TEACH fly inside CLASS Player. ``` +When a script declares classes, `NEW` also checks that the requested class exists. A typo such as `NEW Plaeyr player` receives a repair hint: + +```text +Class Plaeyr is not known yet. Check the class name or declare CLASS Plaeyr before creating player. +``` + +For backward compatibility, a script with no `CLASS` declarations keeps the older permissive `NEW` behavior. + ## Object field assignment checks Claro also has a narrow static diagnostic for direct object-field assignments. If a class declares a typed field and a script creates a simple object with `NEW Class name`, `claro typecheck` remembers the field type: @@ -183,6 +806,28 @@ Output: Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT. ``` +Simple expressions are checked too. Text joined with a number has a `TEXT` result, so this mistake is caught before running the program: + +```claro +SET player.name "Ada" +SET player.score player.name + 1 +``` + +```text +Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT. +``` + +The reverse mismatch is checked as well. A numeric expression cannot be stored in a `TEXT` field: + +```claro +SET player.score 10 +SET player.name player.score + 1 +``` + +```text +Type mismatch for field player.name: expected TEXT, but this value looks like NUMBER. +``` + If a learner assigns to a field the class did not declare, `claro typecheck` now names the object class and suggests the matching `HAS` line: ```claro @@ -275,8 +920,34 @@ Object player is not known yet. Create it with NEW ClassName player before setti Both sides of this narrow field foundation are covered by validation: `tests/typecheck_object_field_good.claro` checks that `SET player.score 10` is accepted for a `HAS score NUMBER` field, `tests/typecheck_object_field_text_good.claro` checks that `SET player.name "Ada"` is accepted for a `HAS name TEXT` field, `tests/typecheck_object_field_yesno_good.claro` checks that `SET player.ready YES` is accepted for a `HAS ready YESNO` field, `tests/typecheck_object_field_unknown_object_bad.claro` checks that `SET player.score 10` before `NEW Player player` reports the missing-object assignment diagnostic, `tests/typecheck_object_field_check_type_unknown_object_bad.claro` checks the matching missing-object `CHECK TYPE` diagnostic, and the remaining object-field fixtures cover known-field mismatches, unknown NUMBER/TEXT/YESNO fields, and unknown-field assignments whose value type is not inferable yet. -This slice is intentionally small: it covers direct `NEW Class object` plus `SET object.field value` cases in one file. Broader object flows, aliases, method return checks, and richer object signatures remain future work. +This slice is intentionally small: it covers direct `NEW Class object` plus `SET object.field value` cases in one file. Field declarations are collected even when a learner writes a simple method before a later `HAS` field, so the type checker can still report the field's declared type. Broader object flows, aliases, method return checks, and richer object signatures remain future work. + +Simple functions also get an arity check before running. If a learner defines `TEACH greet name` and then writes `DO greet` without the `name` argument, `claro typecheck` reports: + +```text +Function greet needs 1 argument, but this call gives 0. Add the missing argument. +``` + +The older compatibility spelling gets the same beginner-facing result when `WITH` is left empty: + +```claro +CALL greet WITH +``` + +Output: + +```text +Function greet needs 1 argument, but this call gives 0. Add the missing argument. +``` + +The same method-aware diagnostic is used when a method directly assigns the wrong value to a class field. For example, `SET name 123` inside `Player.rename` reports `Type mismatch for field name in Player.rename: expected TEXT, but this value looks like NUMBER.` The operator-specific form also remains available: `SET score score + name` inside `Player.add RETURNS NUMBER` reports `Type mismatch for field score in Player.add: addition needs NUMBER values, but name looks like TEXT.` YESNO fields use the same clear method context: assigning text to `HAS ready YESNO` inside `Player.toggle` reports `Type mismatch for field ready in Player.toggle: expected YESNO, but this value looks like TEXT.` Both modern `DO` and compatibility `CALL ... WITH` method forms are covered by the focused validation matrix. These focused checks cover typed method bodies; broader control-flow and object-flow analysis remains planned. ## Status This is currently a static checker feature. It improves `claro typecheck` and validation confidence for `DO` and compatibility `CALL ... WITH` function calls, simple `DO object.method ...` calls where the object was created with `NEW Class name`, and direct assignments to known object fields. Runtime enforcement for every container mutation and richer function/object signatures can be added later after the syntax is classroom-tested. + +Method-body field assignments are covered in both modern and compatibility syntax, including a valid YESNO assignment through `TEACH toggle TAKES value` and `CALL player.toggle WITH YES`. Method-body `CHECK TYPE` metadata has the same balanced coverage: a modern `DO player.toggle YES` example and the compatibility `CALL` example both verify a declared YESNO field. The focused release validator keeps these positive examples beside the wrong-type diagnostics. + +A bare field typo inside a method's `CHECK TYPE` is also checked now. For example, `CHECK TYPE level IS NUMBER` in `Player.train` reports `Object Player has no field level. Check the field name or add HAS level NUMBER to the class.` The older `TAKES` / `LEARNED` method spelling receives the same diagnostic. This keeps method metadata checks consistent with direct method-field assignments and uses the expected type to make the repair actionable. + +Inline method-field annotations are checked for known type names too. For example, `SET score AS BANANA TO 10` inside `Player.set_score` reports `Field score in Player.set_score needs a known type such as NUMBER, TEXT, or YESNO, but BANANA is not a Claro type. Use NUMBER for score.` diff --git a/docs/CLI.md b/docs/CLI.md index a43d1bb..f149245 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -58,7 +58,7 @@ claro package doctor claro package lock ``` -The v1.18.26 package command is a safer local project-file helper. It creates `claro.project`, `claro.lock`, and local folders under `packages/`. It is not yet an online package registry. +The v1.18.26 package command is a safer local project-file helper. It creates `claro.project`, `claro.lock`, and local folders under `packages/`. Package names may appear only once in `claro.project`; `claro package doctor` reports duplicate entries so the lockfile stays unambiguous. It also requires the project manifest to have exactly one non-empty `name:` field. It is not yet an online package registry. ## IDE metadata diff --git a/docs/CURRENT_STATUS.md b/docs/CURRENT_STATUS.md index b526c37..af82925 100644 --- a/docs/CURRENT_STATUS.md +++ b/docs/CURRENT_STATUS.md @@ -51,15 +51,46 @@ Status: **Foundation present** Ready now: - typed variables such as `SET score NUMBER 10` - `TYPE OF` and `CHECK TYPE` -- typed list/map checks through `claro typecheck` -- a narrow function/method argument check: `CHECK TYPE parameter IS TYPE` inside a function or simple object method lets `claro typecheck` accept correct checked calls, catch mismatched `DO`, `CALL ... WITH`, and `DO object.method ...` arguments, explain when a `DO object.method ...` call happens before the object is created with `NEW`, and catch simple calls to undeclared methods with a class-specific `TEACH` hint -- a narrow object-field assignment/check-type check for simple `NEW Class object` plus direct `SET object.field value` and `CHECK TYPE object.field IS TYPE` cases when the class declares `HAS field TYPE`; validation now covers correct NUMBER, TEXT, and YESNO direct assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field `CHECK TYPE` metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, NUMBER/TEXT/YESNO-valued unknown-field diagnostics for direct assignments to undeclared fields, plus a plain beginner-facing unknown-field diagnostic when the assigned expression type is not inferable yet +- `CHECK TYPE` gives a direct repair hint when the learner forgets the `IS` keyword, for example `CHECK TYPE score` reports `CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.` +- `CHECK TYPE` gives a direct repair hint when `IS` has no expected type, for example `CHECK TYPE score IS` reports `CHECK TYPE score needs a type after IS. Try: CHECK TYPE score IS NUMBER.` +- `CHECK TYPE` explains when the learner leaves out the expression as well, for example bare `CHECK TYPE` reports `CHECK TYPE needs an expression, IS, and a type. Try: CHECK TYPE score IS NUMBER.` +- typed function and method returns reject simple extra tokens after a return value with a direct repair hint, such as `RETURN amount extra` reporting `Keep only one expression after RETURN.` This is covered for modern functions and both modern and compatibility object methods. +- `CHECK TYPE` explains when `IS` is present but the expression is missing, for example `CHECK TYPE IS NUMBER` reports `CHECK TYPE needs an expression before IS. Try: CHECK TYPE score IS NUMBER.` +- `CHECK TYPE` rejects extra words after a valid expected type with a direct repair hint, so `CHECK TYPE score IS NUMBER TEXT` explains that only one type belongs in the check +- `CHECK TYPE` rejects unknown expected type names with a beginner-facing list of supported types +- typed function return diagnostics also identify an unknown return expression in compatibility `TAKES` / `LEARNED` functions, matching the modern function and object-method guidance +- `TYPE OF` reports a direct repair hint when the learner forgets `AS`, for example `TYPE OF score` suggests `TYPE OF score AS kind` +- `TYPE OF` reports a direct repair hint when the learner forgets the expression before `AS`, for example `TYPE OF AS kind` reports `TYPE OF needs an expression before AS. Try: TYPE OF score AS kind.` +- `TYPE OF` rejects extra words after its result name with a direct repair hint, so `TYPE OF score AS kind extra` explains that only one result name belongs there; typed `TEACH ... RETURNS TYPE` declarations likewise reject extra words after the return type with a direct repair hint +- typed `TEACH ... RETURNS` declarations reject a missing return type with a direct repair hint, such as `Function greet needs a return type after RETURNS. Add a type such as NUMBER.` The same diagnostic names the complete class and method, including compatibility `TAKES` / `LEARNED` methods: `Method Player.score needs a return type after RETURNS. Add a type such as NUMBER.` +- typed list/map checks through `claro typecheck`, including a nested `LIST OF MAP` insertion example +- compatibility `TAKES` / `LEARNED` functions also have focused positive numeric-addition, subtraction, division, and multiplication return fixtures, keeping arithmetic return coverage aligned with modern functions and compatibility methods. +- compatibility `TAKES` / `LEARNED` functions have positive complete-branch return coverage beside the incomplete-branch diagnostic, so older conditional examples are checked for both accepted and rejected paths. +- a narrow function/method argument check: `CHECK TYPE parameter IS TYPE` inside a function or simple object method lets `claro typecheck` accept correct checked calls, catch mismatched `DO`, `CALL ... WITH`, `DO object.method ...`, and compatibility `CALL ... WITH` arguments, including checked methods that appear after another method in the same class, report missing checked function and method arguments, report missing unchecked arguments for simple functions and simple object methods in both modern `DO` and compatibility `CALL ... WITH` forms, treat empty compatibility calls such as `CALL greet WITH` and `CALL player.rename WITH` as missing-argument mistakes, catch extra arguments to simple functions and checked methods even when the checked method appears after another method in the same class, catch modern `DO` and compatibility `CALL ... WITH` calls to undeclared simple functions, explain when `DO object.method ...` or compatibility `CALL ...` happens before the object is created with `NEW` even if the method name is also wrong, and catch modern plus compatibility calls to undeclared object methods with a class-specific `TEACH` hint +- simple function and object-method return checks with `TEACH name ... RETURNS TYPE` now include a compatibility `TAKES` / `LEARNED` function whose conditional returns are incomplete, preserving the same every-path diagnostic as modern functions. +- function and method declarations now reject repeated parameter names with a direct repair hint before calls are checked, including modern and compatibility `TAKES` / `LEARNED` functions and methods; object methods with the same name are also rejected within a class with a rename hint in both modern and compatibility method syntax +- top-level function declarations now reject repeated names with a direct repair hint, so two `TEACH greet` blocks cannot silently compete for one callable function; a missing function name after `TEACH` gets a direct beginner-facing repair hint, and a missing method name inside `CLASS` names the class and gives a method example +- class declarations now reject a missing class name after `CLASS` with a direct beginner-facing repair hint +- class declarations now reject repeated field names within the same class with a direct repair hint before field types are used, so two `HAS score ...` lines cannot silently choose conflicting metadata; different classes may independently use the same field name +- class field declarations now reject missing names such as bare `HAS`, missing types such as `HAS score`, unknown types such as `HAS score BANANA`, and extra tokens such as `HAS score NUMBER TEXT` with the field name, class name, supported type examples, and a repair hint before object-field checks use that metadata +- class declarations now reject repeated class names with a direct repair hint, so two `CLASS Player` blocks cannot silently compete for the same name; extra words after a class name also get a direct repair hint instead of becoming part of the class identity +- object creation now rejects repeated object names with a direct repair hint, so two `NEW Player player` statements cannot silently replace one another during type checking; when a script declares at least one class, a misspelled `NEW` class name gets a direct declaration hint without changing the older permissive no-class form; a missing class name or object name gets a direct example repair; extra words after the object name get a direct repair hint instead of being silently ignored +- a narrow object-field assignment/check-type check for simple `NEW Class object` plus direct `SET object.field value` and `CHECK TYPE object.field IS TYPE` cases when the class declares `HAS field TYPE`; validation now covers correct NUMBER, TEXT, and YESNO direct assignments, explicitly typed field assignments, explicitly typed method-body assignments to undeclared fields with a `HAS field TYPE` repair hint in modern and compatibility method syntax, dedicated positive NUMBER/TEXT/YESNO `CHECK TYPE` metadata fixtures, simple and chained object aliases for both field assignments and `CHECK TYPE` metadata (including a chained TEXT-field check), aliased object-method calls including chained aliases in modern `DO` and compatibility `CALL ... WITH` forms (with a dedicated positive modern `DO` chained-alias fixture), field-to-field, compound arithmetic field expressions, and arithmetic/text expression result types, negative NUMBER/TEXT/YESNO field `CHECK TYPE` metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type diagnostics, field collection when a `HAS` field appears after a simple method, NUMBER/TEXT/YESNO-valued unknown-field diagnostics for direct assignments to undeclared fields including explicitly typed assignments, numeric and text results from simple arithmetic/text expressions in object-field assignments, plus method-body field assignment and `CHECK TYPE` diagnostics for NUMBER, TEXT, and YESNO fields in modern and compatibility syntax, including compatibility YESNO `CHECK TYPE` metadata coverage, direct method assignments to undeclared fields now produce a beginner-facing missing-field diagnostic when the assigned expression has a known type, and a plain beginner-facing unknown-field diagnostic when the assigned expression is not inferable yet in both modern and compatibility method syntax; method-body `CHECK TYPE` now reports an undeclared bare field with the expected type as a repair hint in both modern and compatibility method syntax, including compatibility `TAKES` / ...; explicitly typed assignments to declared method fields now validate the class-declared field type instead of trusting only the inline type annotation, so a wrong value such as `SET score TEXT "oops"` reports the method and field in the diagnostic Still needed: -- richer typed function signatures and return values +- Keep the focused typecheck validator's fixture list complete as new positive and negative examples are added. `claro validate` now executes the complete focused fixture matrix as well, so release validation cannot silently omit a listed typecheck example. +- The expression checker now carries a known TEXT operand through all arithmetic operators so object-field and typed-function-return diagnostics do not hide addition, subtraction, multiplication, or division mistakes; focused numeric-subtraction, numeric-division, and numeric-multiplication positives plus text-operand addition, subtraction, multiplication, and division negatives protect this behavior. Typed-function return diagnostics now have focused subtraction and division positives alongside multiplication coverage. Text concatenation into a TEXT field has focused positive fixtures for both operand orders, including concatenation of two TEXT object fields. Each numeric operator names the text operand and explains that it needs NUMBER values. +- Method return validation now has focused positive subtraction and division fixtures alongside the existing arithmetic return coverage, including compatibility `TAKES` / `LEARNED` subtraction and division cases, so checked NUMBER method parameters and numeric subtraction or division remain accepted by the release validator in both method spellings. +- Modern and compatibility `TAKES` / `LEARNED` methods have positive NUMBER, TEXT, and YESNO field-assignment coverage, and modern plus compatibility method-body `CHECK TYPE` metadata now have positive YESNO coverage beside the wrong-value diagnostics. +- Modern method-body TEXT concatenation has positive coverage in both operand orders, such as `SET first first + "!"` and `SET last "Dr. " + last`. +- Compatibility method-body TEXT concatenation has matching positive coverage in both operand orders, so `TAKES` / `LEARNED` methods keep the same beginner-safe field update behavior. +- richer typed function signatures and return values beyond the focused simple function/method `RETURNS TYPE` check - type checking through branches and loops - richer object field type checking beyond simple direct assignments - typed imports/modules +- inline method-field annotations must agree with the class `HAS` declaration in both modern `TEACH` / `END` and compatibility `TAKES` / `LEARNED` methods; conflicting annotations now get a repair-oriented diagnostic even when the value's inferred type is otherwise correct. +- inline method-field annotations also reject unknown names such as `BANANA`, naming the field and method and suggesting the class-declared type. +- compatibility `TEACH ... TAKES ...` / `LEARNED` methods have matching validation for unknown inline field annotations, so older lessons receive the same repair guidance. Good starting docs: - `ADVANCED_STATIC_TYPING.md` @@ -96,7 +127,25 @@ Ready now: - `claro package init` - `claro package add/list/remove/doctor/lock` - local project files such as `claro.project`, `claro.lock`, and `packages/` -- package-name safety checks +- starter projects from `claro new` now use the same manifest-version and lock-version headers as `claro package init` +- the default `make` target builds both `claro` and `claro.exe`, so cross-platform release validators exercise freshly built executables +- project-name safety checks for `claro new`, so unsafe names such as `../bad` are rejected before Claro creates folders +- package-name safety checks when adding packages, including the 64-character package-name limit, when `claro package doctor` audits an existing `claro.project`, when `claro package lock` writes lockfile data, when `claro package list` shows existing packages, when `claro package init` sees unsafe names already present in `claro.project`, when `claro package add` sees unsafe names already present in `claro.project`, when `claro package remove` refreshes the lockfile after an edit, when learners need to remove an unsafe package entry that is already present, when `claro package doctor` verifies lockfile checksums for listed packages, and when `claro package doctor` rejects lockfile package entries that are not listed in `claro.project` +- `claro package doctor` rejects a listed package manifest whose `name:` does not match the package name in `claro.project` +- `claro package doctor` compares the complete manifest `name:` value, so a prefix such as `math-tools-extra` cannot be accepted for package `math-tools` +- `claro package doctor` requires exactly one complete supported package `manifest-version: 1` field; missing, blank, unsupported, or duplicate format fields get a `BAD package manifest version` diagnostic with the file path and a repair hint, rather than calling an existing file missing +- `claro package doctor` compares the complete manifest `checksum:` value, so a valid checksum followed by extra text is rejected +- `claro package doctor` requires the complete supported `manifest-version: 1` value in `claro.project`, so a prefix such as `manifest-version: 10` is rejected +- `claro package doctor` requires the complete supported `version: 1` value in each local package manifest, so a prefix such as `version: 10` is rejected +- `claro package doctor` requires exactly one current `version: v1.18.26` line in `claro.lock`, so stale, missing, blank, or duplicate lockfile release versions are rejected +- `claro package doctor` requires the complete supported `source: local` value in each local package manifest, so a prefix such as `source: local-extra` is rejected +- package project entries must be unique; `claro package doctor`, `list`, `add`, `init`, and `lock` reject duplicate names instead of generating ambiguous package or lockfile data +- lockfile package entries must also be unique; `claro package doctor` reports duplicate lock entries instead of accepting ambiguous package/checksum data +- package manifest `name:` entries must also be unique; `claro package doctor` reports `BAD package manifest name` instead of accepting ambiguous package identity data +- package manifest `checksum:` entries must also be unique; `claro package doctor` reports `BAD package checksum` instead of accepting ambiguous integrity data +- package manifest `version:` entries must also be unique and must contain exactly `1`; `claro package doctor` reports `BAD package version` instead of accepting ambiguous package metadata +- project manifests must contain exactly one `manifest-version: 1` field; duplicate or prefix-matching values are rejected with `BAD project manifest version: expected 1` +- project manifests must contain exactly one non-empty `name:` field; `claro package doctor` reports `BAD project manifest name: expected a non-empty name` when the project identity is missing or blank Still needed: - install from local path @@ -154,8 +203,9 @@ Status: **Foundation present** Ready now: - `claro ide` - metadata JSON -- completion list +- completion list, including the current typed-language keywords `RETURNS`, `CHECK`, and `TYPE` - diagnostics helper +- Forgejo/Gitea CI coverage for the current release validation gates (`claro validate`, typecheck diagnostics validation, version convention validation, package security validation, compiler warning validation, and the CI workflow coverage check); the compiler warning validator rebuilds `src/claro.c` and rejects path-truncation warnings, while the workflow validator verifies these commands, including its own command, occur in executable `run` steps rather than comments Still needed: - syntax highlighting package diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index e911aae..b4da557 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -31,21 +31,74 @@ See `CURRENT_STATUS.md` for the detailed feature matrix. 1. Keep beginner-facing docs current and separate from historical release notes. 2. Keep examples aligned with the modern simple syntax (`END`, `DO`, short `SET`, short `ASK`) while documenting older compatibility forms separately. -3. Expand validation around typecheck diagnostics and package/networking safety. Current object-method parameter validation covers one correct `DO object.method ...` call, one wrong-type diagnostic, missing-object guidance when `DO player.method ...` appears before `NEW`, and a class-specific unknown-method diagnostic when a learner calls a method the class does not declare; object-field validation covers correct NUMBER, TEXT, and YESNO direct `SET object.field value` assignments, direct `CHECK TYPE` metadata acceptance for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, missing-object field assignment and `CHECK TYPE` diagnostics, NUMBER/TEXT/YESNO wrong-type direct assignments, simple NUMBER/TEXT/YESNO-valued unknown-field diagnostics after `NEW Class object`, and a beginner-facing fallback when the unknown field's assigned expression type is not inferable yet. -4. Add small examples for each foundation feature before adding bigger syntax. +3. Expand validation around typecheck diagnostics and package/networking safety. Current Forgejo/Gitea CI runs the documented release gates (`claro validate`, typecheck diagnostics validation, version convention validation, package security validation, compiler warning validation, and CI workflow coverage validation). The compiler warning validator rebuilds `src/claro.c` and blocks path-truncation warnings; the workflow coverage validator checks executable `run` steps rather than comments and requires its own command. Current package safety validation covers safe project creation, rejecting unsafe project names during `claro new`, safe manifest/lockfile creation, rejecting unsafe package names during `package add`, detecting unsafe package names already present in `claro.project` during `package doctor`, refusing to write lockfile data for unsafe package names during `package lock`, making `package list` fail instead of displaying unsafe package entries as normal package names, making `package init` fail instead of reporting the project ready when unsafe package names are already present, making `package add` fail before changing files when unsafe package names are already present, making `package remove` fail instead of reporting success when unsafe package names remain during lockfile refresh, allowing `package remove` to remove an exact unsafe package entry so learners can repair a bad `claro.project`, making `package doctor` reject stale lockfile checksums for listed packages, and making `package doctor` reject lockfile package entries that are not listed in `claro.project`. Current function validation covers correct checked calls, wrong-type arguments, missing checked-argument diagnostics, missing unchecked-argument diagnostics for simple functions in both modern `DO greet` and empty compatibility `CALL greet WITH` forms, extra-argument diagnostics, and beginner-facing unknown-function diagnostics for mistyped modern `DO` and compatibility `CALL ... WITH` calls; current object-method parameter validation covers correct modern `DO object.method ...` and compatibility `CALL object.method WITH ...` checked calls, wrong-type diagnostics for both call forms, checked methods after another method in the same class, missing and extra checked-argument diagnostics including the extra-argument case for a checked method after another method in the same class, missing unchecked-argument diagnostics for simple object methods, missing-object guidance when `DO player.method ...` or compatibility `CALL object.method WITH ...` appears before `NEW` even if the method name is also wrong, and class-specific unknown-method diagnostics for both modern and compatibility calls when a learner calls a method the class does not declare; current object-field validation covers direct NUMBER/TEXT/YESNO field-assignment positives, direct object-field `CHECK TYPE` metadata positives for NUMBER/TEXT/YESNO fields, negative NUMBER/TEXT/YESNO field metadata mismatches, NUMBER/TEXT/YESNO-expectation unknown-field `CHECK TYPE` diagnostics, direct wrong-type field assignment diagnostics, field collection when a `HAS` field appears after a simple method, direct unknown-field diagnostics for NUMBER/TEXT/YESNO values, a beginner-facing fallback for unknown fields assigned from expressions whose type is not inferable yet, and missing-object diagnostics for both `SET object.field value` and `CHECK TYPE object.field IS TYPE` before `NEW`. +3a. Keep package diagnostics actionable: invalid package manifest format fields now name the file and explain how to repair it, separately from an absent manifest. Keep package validation honest by checking that the project manifest has one non-empty project name, that the project and each listed package manifest declare the exact supported manifest version, package version, local source, and expected package name, and that lockfiles declare exactly one current release version. +3b. Keep package security validation in Forgejo/Gitea CI so unsafe names, exact manifest-version/name/version/checksum mismatches, missing manifests, duplicate project manifest versions and packages, duplicate lock packages, duplicate package manifest names, versions, and checksums, and lockfile errors remain release blockers. +4. Add small examples for each foundation feature before adding bigger syntax. The object-field foundation now includes positive and negative validation for field expressions such as `SET player.score player.name`, `SET player.score player.score + 1`, `SET player.name player.score + 1`, explicitly typed field assignments, explicit typed assignments to undeclared fields, simple and chained aliases such as `SET alias player`, `SET backup alias`, followed by `SET backup.score ...` or `CHECK TYPE backup.score IS ...`, chained aliases in object-method calls through both modern `DO` and compatibility `CALL ... WITH` forms (including a dedicated positive modern `DO` fixture), and arithmetic/text expression mismatches; typed containers also accept a map with nested type metadata when it is added to a `LIST OF MAP`; broader alias/control-flow checking remains planned. +5. Keep the focused typecheck validator complete: the method-body field diagnostic fixtures are now exercised by `claro validate` alongside the standalone diagnostic validator; continue wiring each new `typecheck_*.claro` fixture into both gates. Modern and compatibility unknown-field expression diagnostics now have matching focused coverage, and explicitly typed unknown method-field assignments have matching modern and compatibility fixtures with the expected `HAS field TYPE` repair hint. +-6. Continue narrowing expression diagnostics: known TEXT operands now remain visible through arithmetic, with focused numeric-subtraction, numeric-division, and numeric-multiplication positives plus text-operand addition, subtraction, multiplication, and division coverage. Typed function returns now include positive subtraction and division examples alongside multiplication, including compatibility `TAKES` / `LEARNED` numeric-addition, subtraction, division, and multiplication return fixtures. Text concatenation into TEXT fields is covered in both operand orders, including field-to-field concatenation. Each numeric operator names the text operand and explains that it needs NUMBER values. Compatibility method return diagnostics now cover addition, subtraction, division, and multiplication. `CHECK TYPE` also rejects misspelled expected type names. +- Keep typed method return examples balanced: numeric addition, subtraction, division, and multiplication now have dedicated positive compatibility `TAKES` / `LEARNED` method fixtures in the focused validator, alongside compatibility addition, subtraction, and division mismatch coverage and the existing method return mismatch diagnostics. +- Keep method field examples balanced across syntax generations: modern and compatibility `TAKES` / `LEARNED` NUMBER, TEXT, and YESNO assignments now have positive fixtures beside the modern and compatibility mismatch fixtures, and modern plus compatibility YESNO method-field metadata now have dedicated positive fixtures beside the negative fixture. +- Keep IDE metadata aligned with the current beginner syntax: typed-language keywords such as `RETURNS`, `CHECK`, and `TYPE` are now included in the metadata used by editor helpers. +- Keep compatibility-call coverage aligned with modern calls: empty `CALL object.method WITH` forms now have a focused missing-argument diagnostic fixture alongside the modern `DO object.method` case. +Keep declared return types honest: `claro typecheck` now reports friendly diagnostics for unknown declared return types, missing return types, mismatched, not-yet-inferable, and empty return expressions (including compatibility `TAKES` / `LEARNED` methods), declarations with no `RETURN` in modern and compatibility function forms, and incomplete conditional branches, when a simple function or object method declares `RETURNS TYPE`; missing-type coverage includes modern functions and both modern and compatibility methods, with a compatibility-function fixture as well. Incomplete conditional-return coverage now also includes the compatibility `TAKES` / `LEARNED` function spelling, so older lessons receive the same every-path diagnostic. The declaration keyword is case-insensitive like other Claro keywords, with positive coverage in modern and compatibility function forms, including lowercase compatibility declarations and lowercase compatibility method declarations, plus a lowercase compatibility-method mismatch fixture. Complete `IF`/`ELSE` return branches, including nested complete conditionals in functions and methods, are accepted, and modern and compatibility function syntax plus compatibility method syntax are covered by focused fixtures. `CHECK TYPE` parameter metadata also informs arithmetic return-expression checking, including object methods; positive numeric multiplication return coverage now sits beside the mismatch fixtures, including a dedicated compatibility-method multiplication success fixture; typed methods can also return class-declared fields by simple name, with positive and mismatch fixtures in both modern and compatibility method syntax, and unknown method return expressions now have dedicated modern and compatibility diagnostic fixtures, and NUMBER return expressions with known TEXT arithmetic operands identify the operation and offending operand, including focused addition, subtraction, multiplication, and division coverage for functions and methods. Compatibility method subtraction diagnostics are also covered alongside the existing addition diagnostic. Full path-sensitive analysis across nested conditionals and loops remains planned. -## Complete-platform milestones +7. Keep method-body field diagnostics learner-facing: typed methods now name the class method when a simple class field receives a known mismatch in `CHECK TYPE` or assignment, or a known TEXT operand in a NUMBER expression, with matching text and numeric coverage in modern and compatibility method syntax; direct assignments to undeclared method fields now report the missing field when the value type is known, and bare `CHECK TYPE missing IS TYPE` statements inside methods now use the expected type in the repair hint in both modern and compatibility method syntax; broader control-flow and object-flow analysis remains planned. +8: Keep method-body coverage balanced across beginner field types and syntax generations: YESNO field assignment mismatches are now validated for both modern `DO` and compatibility `CALL ... WITH` method syntax, method-body `CHECK TYPE` metadata mismatches now have matching compatibility `TAKES` / `LEARNED` coverage alongside the modern form, compatibility YESNO field metadata has dedicated positive and negative fixtures, and compatibility method assignments to undeclared fields have the same focused missing-field diagnostic as modern methods, including a beginner-facing fallback when the assigned expression is not inferable yet. +8a. Keep method-body text expressions balanced: modern TEXT field concatenation is covered when the field is on either side of `+`, so both common beginner word-order patterns remain accepted. +8b. Keep compatibility method-body text expressions aligned: `TAKES` / `LEARNED` methods now have positive TEXT concatenation coverage in both operand orders beside the modern `TEACH` / `END` examples. +8c. Keep explicit method-field annotations aligned with class declarations: `SET score TEXT "oops"` inside a method with `HAS score NUMBER` now reports the declared field mismatch instead of accepting the inline annotation; broader annotation consistency remains planned. +8d. Keep inline method-field annotations consistent with `HAS` declarations: `claro typecheck` now rejects a conflicting annotation even when the assigned value itself has the class-declared type, and explains which type to use. +8e. Keep inline method-field annotation coverage aligned across syntax generations: compatibility `TAKES` / `LEARNED` methods now have matching positive and negative fixtures, so older lessons retain the same class-declared-type guidance. +8f. Keep inline method-field annotations learner-facing: an unknown annotation such as `BANANA` now names the field and method and suggests the `HAS` type instead of silently treating the annotation as an expression. +8g. Keep unknown inline method-field annotation diagnostics aligned across syntax generations: compatibility `TAKES` / `LEARNED` methods now have matching focused coverage for misspelled annotations. +8h. Keep function and method declarations unambiguous: `claro typecheck` now reports repeated parameter names with a repair hint, before duplicate names can make argument diagnostics confusing, including modern and compatibility `TAKES` / `LEARNED` functions and methods. Duplicate object method names within one class now get the same declaration-time protection and a rename hint, with focused coverage for both modern and compatibility method syntax. Broader signature validation remains planned. +8h.1. Keep top-level function declarations unambiguous: duplicate `TEACH greet` blocks now get a declaration-time diagnostic with a rename hint, preventing two functions from competing for one name. Broader signature validation remains planned. +8h.2. Keep function declarations understandable: a bare `TEACH` now gets a direct diagnostic explaining that a function name is required, with a small example repair. Broader signature validation remains planned. +8h.3. Keep method declarations understandable: a bare `TEACH` inside a class now names the class and explains how to add a method name, instead of using the less-specific top-level function diagnostic. +8i. Keep class declarations unambiguous: duplicate `HAS` field names within one class now get a declaration-time diagnostic with a repair hint, preventing conflicting field types from being silently accepted, while same-named fields in different classes remain valid. Broader object declaration validation remains planned. +8j. Keep class names unambiguous: duplicate `CLASS Player` declarations now get a declaration-time diagnostic with a rename hint, preventing two class definitions from competing for one name. Extra words after a class name also get a declaration-time repair hint, so a typo such as `CLASS Player Extra` cannot silently create a surprising class identity. + +8k. Keep object names unambiguous: repeated `NEW Player player` statements now get a declaration-time diagnostic with a rename hint, preventing one object type environment from silently replacing another. + +8l. Keep object creation understandable: when classes are declared in a script, an unknown `NEW` class name now gets a direct class-name diagnostic and repair hint; the older permissive `NEW` behavior remains when no class declarations are present. + +8m. Keep class field metadata understandable: a missing or unknown type in a `HAS field TYPE` declaration, or extra text after a valid type, now gets a class-and-field diagnostic with supported type examples or a direct repair hint before object-field checks use that metadata. +8m.1. Keep class field declarations understandable: a bare `HAS` now gets a direct diagnostic explaining that both a field name and type are required, with a small `HAS score NUMBER` repair example. +8n. Keep object creation unambiguous: extra words after `NEW ClassName object` now get a direct repair hint, so a malformed object declaration cannot silently discard part of the learner's input. + +8o. Keep class declarations understandable: a bare `CLASS` now gets a direct diagnostic explaining that a class name is required, with a small example repair. + +8p. Keep object declarations understandable: a `NEW` statement with a class but no object name now gets a direct diagnostic with a complete `NEW Player player` repair example; a bare `NEW` now separately explains that both a class and object name are required. + +8q. Keep `CHECK TYPE` syntax understandable: a missing `IS` now gets a direct repair hint with a complete `CHECK TYPE score IS NUMBER` example. + +8q.1. Keep `CHECK TYPE` declarations understandable: `CHECK TYPE score IS` now gets a direct repair hint naming the missing expected type and showing a complete example. + +8q.2. Keep `CHECK TYPE` declarations unambiguous: extra words after a valid expected type now get a direct repair hint instead of being reported as an unknown type. +8q.3. Keep `CHECK TYPE` declarations complete: a bare `CHECK TYPE` now explains that the expression, `IS`, and expected type are all required, with a complete repair example. +8q.4. Keep `CHECK TYPE` declarations ordered: `CHECK TYPE IS NUMBER` now explains that an expression belongs before `IS`, with a complete repair example. +8q.5. Keep `TYPE OF` declarations understandable: a missing `AS` now gets a direct repair hint with a complete `TYPE OF score AS kind` example. +8q.5.1. Keep `TYPE OF` declarations complete: a missing expression before `AS` now gets a direct repair hint with a complete `TYPE OF score AS kind` example. +8q.6. Keep `TYPE OF` declarations unambiguous: extra words after the result name now get a direct repair hint instead of being silently included in the variable name. +8q.7. Keep typed `TEACH` declarations unambiguous: extra words after a declared return type now get a direct repair hint instead of being treated as part of an unknown type. +8q.8. Keep typed `TEACH` declarations complete: a bare `RETURNS` now gets a direct repair hint naming the missing type and showing a supported example such as `NUMBER`. +8q.9. Keep missing return-type diagnostics aligned across functions and methods: modern and compatibility methods now name the complete `Class.method` identity when `RETURNS` has no type, with focused fixtures in the typecheck and release validation matrices. +8q.10. Keep return statements beginner-readable: typed returns now identify simple extra tokens after a value and explain that `RETURN` accepts one expression, with focused release validation coverage. +8q.11. Keep extra-token return diagnostics aligned across syntax generations: modern and compatibility functions and object methods now have focused fixtures for `RETURN value extra`, so errors keep the function or class-and-method name and the same repair hint. +8q.12. Keep empty typed-return diagnostics aligned across syntax generations: modern and compatibility functions and object methods now have focused fixtures for bare `RETURN`, so each form explains that the declared type needs an expression. +8q.13. Keep unknown typed-return diagnostics aligned across syntax generations: compatibility `TAKES` / `LEARNED` functions now have a focused fixture for an uninferable return expression, matching modern functions and methods. ### 1. Strong static types Goal: make larger beginner programs safer without making first scripts harder. Needed next: -- richer typed function signatures -- typed function returns +- richer typed function signatures beyond the focused simple `RETURNS TYPE` check +- typed function returns beyond the focused simple function/method `RETURNS TYPE` check - type checking across branches and loops - typed imports/modules -- broader object field checking and method return typing +- broader object field checking and richer method return typing - clearer error messages for type mismatches ### 2. Objects and classes polish @@ -54,7 +107,7 @@ Goal: keep object-oriented examples readable enough for beginners. Needed next: - constructor-style defaults or beginner-friendly initialization helpers -- method return checks +- richer method return checks beyond the focused simple `RETURNS TYPE` check - object printing/debugging helpers - better examples that avoid abstract toy OOP diff --git a/docs/TESTING.md b/docs/TESTING.md index 804d9f7..35d7508 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -24,6 +24,7 @@ Validate the current v1.18.26 package after building: ./claro validate python tools/validate_typecheck_diagnostics.py python tools/validate_version_convention.py +python tools/validate_ci_workflow.py ``` Useful focused validation scripts: @@ -39,6 +40,8 @@ python tools/validate_lsp_helper.py python tools/validate_package_security.py ``` +The Forgejo/Gitea CI workflow is expected to run the current release gates above, not only the older smoke tests. This includes `tools/validate_compiler_warnings.py`, which rebuilds the C source as an object file and rejects the known path-truncation warning class. `tools/validate_ci_workflow.py` checks that coverage so CI does not drift away from the documented package validation path. + Older `validate_rc*.py` scripts are kept for historical release notes. They are not the recommended current package validation path. On Windows, after `build.bat` or `build.ps1`, run the same Python commands from the project root. diff --git a/docs/V1_16_PACKAGES_PROJECTS.md b/docs/V1_16_PACKAGES_PROJECTS.md index f499e15..70b0f7a 100644 --- a/docs/V1_16_PACKAGES_PROJECTS.md +++ b/docs/V1_16_PACKAGES_PROJECTS.md @@ -63,9 +63,35 @@ source: local checksum: 1234abcd ``` -`claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. +`claro.lock` records the release version, lock format, packages, and checksums so future registry work has a stable safety foundation. `claro package doctor` checks those lockfile checksums for listed packages and reports `BAD lock checksum: name` if the lockfile is stale or edited incorrectly. It rejects duplicate `package:` entries with `DUPLICATE lock package: name`, so one package cannot have ambiguous lock data. It also reports `BAD lock package not in claro.project: name` when the lockfile contains a package entry that is not listed in `claro.project`, so learners know to refresh the lockfile instead of trusting stale package data. The project file must declare the complete supported `manifest-version: 1` value; a prefix such as `manifest-version: 10` is rejected as `BAD project manifest version: expected 1`. Package manifests must declare exactly one complete supported `manifest-version: 1` value. Missing, blank, unsupported (such as `10`), or duplicate format fields report `BAD package manifest version` with the package name, the manifest path, and a hint to keep exactly one `manifest-version: 1` line. The doctor leaves the file unchanged for you to repair; an absent manifest file still reports `MISSING package manifest`. They must also declare exactly one expected `name:` value; duplicate or prefix-matching names are rejected as `BAD package manifest name: name`. The manifest must contain exactly one checksum field with the expected complete value; duplicate, trailing, or extra checksum text is rejected as `BAD package checksum: name`. Package manifests must also declare exactly one `version: 1` field; duplicate or prefix-matching versions such as `version: 10` are rejected as `BAD package version: name`. Package manifests must also declare the complete supported local `source: local` value. Prefixes such as `source: local-extra` are rejected as `BAD package source: name` rather than being accepted as valid local manifests. -## Safety rules +If `package doctor` reports `BAD package source: math-tools`, open `packages/math-tools/claro.package` and keep exactly one `source: local` line. Duplicate `source:` lines are rejected even when both say `local`, or when a valid line appears before or after an unsupported source. The doctor leaves the file unchanged so you can review and repair it yourself. + +If `package doctor` reports `BAD lock checksum: math-tools`, check the `math-tools` entry in `claro.lock`. Each package must have exactly one matching `checksum:` line. Duplicate checksums are rejected even when they agree, or when a correct checksum appears before or after an incorrect one. The doctor leaves the file unchanged. After reviewing your project and package manifests, run `claro package lock` to regenerate the lockfile, then run `claro package doctor` again. + +## Lock format safety + +`claro package doctor` requires exactly one `lock-version: 1` line in `claro.lock`. Missing, empty, unsupported (such as `10`), or duplicate format versions now fail with: + +```text +BAD lock version: expected exactly one lock-version: 1 line +``` + +The lockfile must also contain exactly one current release line, `version: v1.18.26`. A missing, stale, blank, or duplicate release line fails with: + +```text +BAD lock release version: expected exactly one version: v1.18.26 line +``` + +The doctor leaves your files unchanged. After reviewing your project and package manifests, run `claro package lock` to regenerate the lockfile, then run `claro package doctor` again. Spaces around values and capitalization of the lock-format field name are accepted, as with other lockfile fields. Valid generated lockfiles continue to work unchanged; this check does not add registry downloads or verify package content hashes. + +## Project-name safety + +`claro new NAME` checks the project name before creating folders. Project names may use only letters, numbers, dash, and underscore, and they must be 64 characters or fewer. + +This keeps mistakes like `../bad` from creating a project outside the folder where the learner is working. + +## Package-name safety Package names may use only: @@ -76,6 +102,8 @@ dash underscore ``` +Package names must be 64 characters or fewer so Claro can create predictable local package folders and metadata paths. + This means simple names like these are allowed: ```text @@ -93,6 +121,8 @@ folder/name bad name ``` +If an unsafe package name somehow gets into `claro.project`, package maintenance commands should not quietly continue. `claro package doctor` points out the bad name, `claro package lock` refuses to write lockfile data for it, `claro package list` refuses to display it as a normal package, `claro package init` refuses to report the project ready while the unsafe entry remains, and `claro package remove NAME` exits with an error if the lockfile refresh still sees an unsafe package name after the removal. Learners can still recover by removing the exact unsafe entry, such as `claro package remove ../bad`; Claro rewrites the project file and lockfile without using that unsafe name as a folder path. + ## Why this matters Packages are important for Claro's future, but the workflow must stay friendly: diff --git a/src/claro.c b/src/claro.c index e878666..1a5f9a8 100644 --- a/src/claro.c +++ b/src/claro.c @@ -322,7 +322,7 @@ static Value builtin_call(Runtime *rt,const char *name,Value *args,int argc,int /* ---------- execution ---------- */ static void exec_range(Runtime *rt,Program *p,int start,int end); static int parse_params(char *s,char ***out){ char **arr=NULL; int cnt=0,cap=0; char *tok=strtok(s,","); while(tok){ char *t=trim_inplace(tok); if(*t){ if(cnt>=cap){cap=cap?cap*2:4; arr=(char**)xrealloc(arr,sizeof(char*)*cap);} arr[cnt++]=xstrdup(t);} tok=strtok(NULL,","); } *out=arr; return cnt; } -static void parse_teach_parts(char *t,char **name_out,char ***params_out,int *pcnt_out){ char *name,*takes; char **params=NULL; int pcnt=0; t+=5; while(*t&&isspace((unsigned char)*t)) t++; name=t; while(*t&&!isspace((unsigned char)*t)) t++; if(*t) *t++=0; takes=strstr(t,"TAKES"); if(takes){ takes+=5; pcnt=parse_params(takes,¶ms); } else { char *simple=trim_inplace(t); if(*simple) pcnt=parse_params(simple,¶ms); } *name_out=name; *params_out=params; *pcnt_out=pcnt; } +static void parse_teach_parts(char *t,char **name_out,char ***params_out,int *pcnt_out){ char *name,*takes,*returns; char **params=NULL; int pcnt=0; t+=5; while(*t&&isspace((unsigned char)*t)) t++; name=t; while(*t&&!isspace((unsigned char)*t)) t++; if(*t) *t++=0; returns=(char*)find_word_ci(t,"RETURNS"); if(returns) *returns=0; takes=(char*)find_word_ci(t,"TAKES"); if(takes){ takes+=5; pcnt=parse_params(takes,¶ms); } else { char *simple=trim_inplace(t); if(*simple) pcnt=parse_params(simple,¶ms); } *name_out=name; *params_out=params; *pcnt_out=pcnt; } static void scan_functions(Runtime *rt,Program *p,const char *prefix){ int i; for(i=0;icount;i++){ char tmp[1024],up[64],w[128]; char *t; strncpy(tmp,p->lines[i],sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(strcmp(up,"CLASS")==0){ const char *pcur=t+5; char *cls=unquote_token(&pcur); char *cn=trim_inplace(cls); int end=match_block(p,i,"CLASS","ENDCLASS",NULL,NULL); ClassDef *c=NULL; int j; if(*cn) c=add_class(rt,cn); for(j=i+1;j<(end<0?p->count:end);j++){ char itmp[1024],iup[64],iw[128]; char *it; strncpy(itmp,p->lines[j],sizeof(itmp)-1); itmp[sizeof(itmp)-1]=0; it=trim_inplace(itmp); first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); if(strcmp(iup,"HAS")==0){ const char *fp=it+3; char *fn=unquote_token(&fp); char *ft=unquote_token(&fp); char *ftt=trim_inplace(ft); if(c) class_add_field(c,trim_inplace(fn),*ftt?ftt:"ANY"); free(fn); free(ft); } else if(strcmp(iup,"TEACH")==0){ char *mname; char **params=NULL; int pcnt=0; char fname[256]; int mend=match_block(p,j,"TEACH","LEARNED",NULL,NULL); parse_teach_parts(it,&mname,¶ms,&pcnt); if(c&&*mname){ snprintf(fname,sizeof(fname),"%s.%s",c->name,mname); if(!find_function(rt,fname)) add_function(rt,fname,params,pcnt,p,j+1,mend<0?(end<0?p->count:end):mend); } if(mend>=0) j=mend; } } free(cls); if(end>=0) i=end; continue; } if(strcmp(up,"TEACH")==0){ char *name; char **params=NULL; int pcnt=0; char fname[256]; int end=match_block(p,i,"TEACH","LEARNED",NULL,NULL); parse_teach_parts(t,&name,¶ms,&pcnt); snprintf(fname,sizeof(fname),"%s%s",prefix?prefix:"",name); if(!find_function(rt,fname)) add_function(rt,fname,params,pcnt,p,i+1,end<0?p->count:end); if(end>=0) i=end; } } } @@ -371,8 +371,8 @@ static void exec_line(Runtime *rt,Program *p,int *pcp,const char *raw){ char buf } rt_error(rt,p->path,pc+1,"HTTP needs GET, STATUS, SAVE, or CHECK. Try: HTTP GET \"claro://hello\" AS page"); return; } if(strcmp(up,"OBJECT")==0){ const char *as=find_word_ci(t,"AS"); if(as && starts_ci(t+6," CLASS")){ char *oe=substr(t+12,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value ov=eval_expr(rt,oe); const char *cn=object_class_name(ov); rt_set_checked(rt,p->path,pc+1,var,"TEXT",v_str(cn?cn:"")); free(oe); free(var); } else if(as && starts_ci(t+6," FIELDS")){ char *oe=substr(t+13,as); char *var=xstrdup(trim_inplace((char*)as+2)); Value ov=eval_expr(rt,oe); Value arr=v_list(); int i; if(ov.type==V_MAP&&ov.map){ for(i=0;icount;i++){ if(strcmp(ov.map->keys[i],"class")!=0 && !starts_ci(ov.map->keys[i],"__type_")) list_add(arr.list,v_str(ov.map->keys[i])); } } rt_set_checked(rt,p->path,pc+1,var,"LIST",arr); free(oe); free(var); } else rt_error(rt,p->path,pc+1,"OBJECT needs CLASS or FIELDS. Try: OBJECT CLASS player AS kind"); return; } - if(strcmp(up,"TYPE")==0 && starts_ci(t,"TYPE OF")){ const char *as=find_word_ci(t,"AS"); if(as){ char *expr=substr(t+7,as); char *var=xstrdup(trim_inplace((char*)as+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); rt_set_checked(rt,p->path,pc+1,var,"TEXT",v_str(decl?decl:claro_value_type(v))); free(expr); free(var); } else rt_error(rt,p->path,pc+1,"TYPE OF needs AS. Try: TYPE OF score AS kind"); return; } - if(strcmp(up,"CHECK")==0 && starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *ty=xstrdup(trim_inplace((char*)is+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); if(decl && !ci_eq(decl,ty) && !ci_eq(ty,"ANY")){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but %s is %s.",ty,trim_inplace(expr),decl); } else if(!decl && !claro_type_matches(ty,v)){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but got %s.",ty,claro_value_type(v)); } free(expr); free(ty); } else rt_error(rt,p->path,pc+1,"CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER"); return; } + if(strcmp(up,"TYPE")==0 && starts_ci(t,"TYPE OF")){ const char *as=find_word_ci(t,"AS"); if(as){ char *expr=substr(t+7,as); const char *pcur=as+2; char *var=unquote_token(&pcur); char *extra=xstrdup(trim_inplace((char *)pcur)); if(extra&&*extra) rt_error(rt,p->path,pc+1,"TYPE OF %s has extra text after result name %s. Keep only the expression, AS, and one result name.",trim_inplace(expr),trim_inplace(var)); else { char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); rt_set_checked(rt,p->path,pc+1,var,"TEXT",v_str(decl?decl:claro_value_type(v))); } free(expr); free(var); free(extra); } else rt_error(rt,p->path,pc+1,"TYPE OF needs AS. Try: TYPE OF score AS kind"); return; } + if(strcmp(up,"CHECK")==0 && starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *ty=xstrdup(trim_inplace((char*)is+2)); char dbuf[320]; const char *decl=declared_type_for_expr(rt,expr,dbuf,sizeof(dbuf)); Value v=eval_expr(rt,expr); if(decl && !ci_eq(decl,ty) && !ci_eq(ty,"ANY")){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but %s is %s.",ty,trim_inplace(expr),decl); } else if(!decl && !claro_type_matches(ty,v)){ rt_error(rt,p->path,pc+1,"Type check failed: expected %s, but got %s.",ty,claro_value_type(v)); } free(expr); free(ty); } else rt_error(rt,p->path,pc+1,"CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER."); return; } if(strcmp(up,"SET")==0){ char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); if(as&&to&&aspath,pc+1,n,ty,v); free(name); free(type); } else if(to){ char *name=substr(rest,to); char *n=trim_inplace(name); char *space=strchr(n,' '); const char *type=NULL; if(space){ *space=0; if(claro_is_type_word(trim_inplace(space+1))) type=trim_inplace(space+1); else { *space=' '; } } { Value v=eval_expr(rt,to+2); rt_set_checked(rt,p->path,pc+1,n,type,v); } free(name); } else { const char *pcur=rest; char *name=unquote_token(&pcur); char *n=trim_inplace(name); const char *save=pcur; char *maybe=unquote_token(&pcur); char *ty=trim_inplace(maybe); if(claro_is_type_word(ty)){ char *after=trim_inplace((char*)pcur); Value v=*after?eval_expr(rt,after):claro_default_for_type(ty); if(*n) rt_set_checked(rt,p->path,pc+1,n,ty,v); } else { Value v=eval_expr(rt,save); if(*n) rt_set_checked(rt,p->path,pc+1,n,NULL,v); } free(maybe); free(name); } return; } @@ -529,33 +529,134 @@ static int check_file(const char *path){ static const char *type_env_get(Var *types,const char *name){ Var *v=env_find(types,name); if(v&&v->val.type==V_STR) return v->val.str; return NULL; } static void type_env_set(Var **types,const char *name,const char *type){ Var *v; char tmp[256]; char *t; if(!name||!type||!*type) return; strncpy(tmp,name,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(!*t) return; v=env_find(*types,t); if(v){ v->val=v_str(type); return; } v=(Var*)xmalloc(sizeof(Var)); memset(v,0,sizeof(Var)); v->name=xstrdup(t); v->val=v_str(type); v->next=*types; *types=v; } -static const char *simple_expr_type(Var *types,const char *expr){ char tmp[512]; char *t; double d; size_t i=0; if(!expr) return NULL; strncpy(tmp,expr,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(!*t) return NULL; if(*t=='"') return "TEXT"; if(parse_number_strict(t,&d)) return "NUMBER"; if(ci_eq(t,"YES")||ci_eq(t,"NO")||ci_eq(t,"TRUE")||ci_eq(t,"FALSE")) return "YESNO"; if(ci_eq(t,"LIST")) return "LIST"; if(ci_eq(t,"MAP")) return "MAP"; if(starts_ci(t,"NEW ")) return "OBJECT"; while(t[i]&&(isalnum((unsigned char)t[i])||t[i]=='_'||t[i]=='.')) i++; if(i>0 && t[i]==0){ return type_env_get(types,t); } return NULL; } -static int type_words_match(const char *need,const char *got){ if(!need||!*need||ci_eq(need,"ANY")) return 1; if(!got||!*got) return 1; if(ci_eq(need,got)) return 1; if(starts_ci(need,"LIST OF") && ci_eq(got,"LIST")) return 1; if(starts_ci(need,"MAP OF") && ci_eq(got,"MAP")) return 1; if((ci_eq(need,"YESNO")||ci_eq(need,"BOOL")||ci_eq(need,"BOOLEAN")) && (ci_eq(got,"YESNO")||ci_eq(got,"BOOL")||ci_eq(got,"BOOLEAN"))) return 1; if(ci_eq(need,"OBJECT") && (ci_eq(got,"MAP")||starts_ci(got,"OBJECT:"))) return 1; return 0; } +static const char *simple_expr_type(Var *types,const char *expr){ + char tmp[512]; char *t; double d; size_t i=0; int in_string=0; + if(!expr) return NULL; + strncpy(tmp,expr,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; + t=trim_inplace(tmp); if(!*t) return NULL; + if(*t=='"') return "TEXT"; + if(parse_number_strict(t,&d)) return "NUMBER"; + if(ci_eq(t,"YES")||ci_eq(t,"NO")||ci_eq(t,"TRUE")||ci_eq(t,"FALSE")) return "YESNO"; + if(ci_eq(t,"LIST")) return "LIST"; + if(ci_eq(t,"MAP")) return "MAP"; + if(starts_ci(t,"NEW ")) return "OBJECT"; + for(i=0;t[i];i++){ + if(t[i]=='"') in_string=!in_string; + if(!in_string&&strchr("+-*/",t[i])){ + char *left=substr(t,t+i), *right=xstrdup(t+i+1); const char *lt,*rt; + lt=simple_expr_type(types,left); rt=simple_expr_type(types,right); + free(left); free(right); + if((lt&&ci_eq(lt,"TEXT"))||(rt&&ci_eq(rt,"TEXT"))) return "TEXT"; + if(lt&&rt&&ci_eq(lt,"NUMBER")&&ci_eq(rt,"NUMBER")) return "NUMBER"; + return NULL; + } + } + i=0; while(t[i]&&(isalnum((unsigned char)t[i])||t[i]=='_'||t[i]=='.')) i++; + if(i>0 && t[i]==0) return type_env_get(types,t); + return NULL; +} +static int return_value_extra_tokens(const char *expr,char *value,size_t value_size){ + char tmp[512],*t,*p; + if(!expr||!value||value_size==0) return 0; + strncpy(tmp,expr,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; + t=trim_inplace(tmp); + if(!*t||strpbrk(t,"+-*/\"")!=NULL) return 0; + p=t; + while(*p&&!isspace((unsigned char)*p)) p++; + if(!*p) return 0; + *p++=0; + while(*p&&isspace((unsigned char)*p)) p++; + if(!*p) return 0; + snprintf(value,value_size,"%s",t); + return 1; +} +static const char *text_operand_for_operator(Var *types,const char *expr,char wanted){ static char operand[256]; char tmp[512]; char *t; size_t i; int in_string=0; if(!expr) return NULL; strncpy(tmp,expr,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); for(i=0;t[i];i++){ char *left,*right; const char *lt,*rt; if(t[i]=='"') in_string=!in_string; if(!in_string&&t[i]==wanted){ left=substr(t,t+i); right=xstrdup(t+i+1); lt=simple_expr_type(types,left); rt=simple_expr_type(types,right); if(lt&&ci_eq(lt,"TEXT")){ snprintf(operand,sizeof(operand),"%s",trim_inplace(left)); free(left); free(right); return operand; } if(rt&&ci_eq(rt,"TEXT")){ snprintf(operand,sizeof(operand),"%s",trim_inplace(right)); free(left); free(right); return operand; } free(left); free(right); } } return NULL; } +static char text_operator_for_number_return(Var *types,const char *expr){ const char ops[]="+-*/"; int i; for(i=0;ops[i];i++) if(text_operand_for_operator(types,expr,ops[i])) return ops[i]; return 0; } +static int type_words_match(const char *need,const char *got){ if(!need||!*need||ci_eq(need,"ANY")||ci_eq(got,"ANY")) return 1; if(!got||!*got) return 1; if(ci_eq(need,got)) return 1; if((ci_eq(need,"LIST")||ci_eq(need,"MAP")) && starts_ci(got,need)) return 1; if(starts_ci(need,"LIST OF") && ci_eq(got,"LIST")) return 1; if(starts_ci(need,"MAP OF") && ci_eq(got,"MAP")) return 1; if((ci_eq(need,"YESNO")||ci_eq(need,"BOOL")||ci_eq(need,"BOOLEAN")) && (ci_eq(got,"YESNO")||ci_eq(got,"BOOL")||ci_eq(got,"BOOLEAN"))) return 1; if(ci_eq(need,"OBJECT") && (ci_eq(got,"MAP")||starts_ci(got,"OBJECT:"))) return 1; return 0; } static const char *container_member_type(const char *type,const char *kind){ const char *p; static char buf[128]; if(!type||!kind) return NULL; if(!starts_ci(type,kind)) return NULL; p=type+strlen(kind); while(*p&&isspace((unsigned char)*p)) p++; if(!starts_ci(p,"OF")) return NULL; p+=2; while(*p&&isspace((unsigned char)*p)) p++; if(!*p) return NULL; snprintf(buf,sizeof(buf),"%s",p); return trim_inplace(buf); } static void parse_set_for_typecheck2(const char *t,char **name,char **type,char **expr){ const char *rest=t+3; const char *to=find_word_ci(rest,"TO"); const char *as=find_word_ci(rest,"AS"); *name=NULL; *type=NULL; *expr=NULL; if(as&&to&&asfunc=xstrdup(func); c->param=xstrdup(param); c->type=xstrdup(type); c->pos=pos; c->next=*checks; *checks=c; } static const char *type_param_check_for(TypeParamCheck *checks,const char *func,int pos,const char **param){ TypeParamCheck *c=checks; while(c){ if(c->pos==pos&&ci_eq(c->func,func)){ if(param) *param=c->param; return c->type; } c=c->next; } return NULL; } +static void add_type_arity_check(TypeArityCheck **checks,const char *func,int arity){ TypeArityCheck *c; if(!func||!*func) return; c=(TypeArityCheck*)xmalloc(sizeof(TypeArityCheck)); c->func=xstrdup(func); c->arity=arity; c->next=*checks; *checks=c; } +static int type_arity_check_for(TypeArityCheck *checks,const char *func){ TypeArityCheck *c=checks; while(c){ if(ci_eq(c->func,func)) return c->arity; c=c->next; } return -1; } +static void add_type_return_check(TypeReturnCheck **checks,const char *func,const char *type){ TypeReturnCheck *c; if(!func||!*func||!type||!*type) return; c=(TypeReturnCheck*)xmalloc(sizeof(TypeReturnCheck)); c->func=xstrdup(func); c->type=xstrdup(type); c->next=*checks; *checks=c; } +static const char *type_return_check_for(TypeReturnCheck *checks,const char *func){ TypeReturnCheck *c=checks; while(c){ if(ci_eq(c->func,func)) return c->type; c=c->next; } return NULL; } static void add_type_field_check(TypeFieldCheck **checks,const char *cls,const char *field,const char *type){ TypeFieldCheck *c; if(!cls||!*cls||!field||!*field||!type||!*type) return; c=(TypeFieldCheck*)xmalloc(sizeof(TypeFieldCheck)); c->cls=xstrdup(cls); c->field=xstrdup(field); c->type=xstrdup(type); c->next=*checks; *checks=c; } static const char *type_field_check_for(TypeFieldCheck *checks,const char *cls,const char *field){ TypeFieldCheck *c=checks; while(c){ if(ci_eq(c->cls,cls)&&ci_eq(c->field,field)) return c->type; c=c->next; } return NULL; } static void add_type_method_check(TypeMethodCheck **checks,const char *cls,const char *method){ TypeMethodCheck *c; if(!cls||!*cls||!method||!*method) return; c=(TypeMethodCheck*)xmalloc(sizeof(TypeMethodCheck)); c->cls=xstrdup(cls); c->method=xstrdup(method); c->next=*checks; *checks=c; } static int type_method_check_for(TypeMethodCheck *checks,const char *cls,const char *method){ TypeMethodCheck *c=checks; while(c){ if(ci_eq(c->cls,cls)&&ci_eq(c->method,method)) return 1; c=c->next; } return 0; } +static int type_method_name_exists(TypeMethodCheck *checks,const char *method){ TypeMethodCheck *c=checks; while(c){ if(ci_eq(c->method,method)) return 1; c=c->next; } return 0; } static int param_index_named(char **params,int pcnt,const char *name){ int i; char tmp[256]; char *p; for(i=0;i=0) add_type_param_check(checks,func_name,en,need,idx); free(expr); free(need); } } } } static TypeParamCheck *collect_function_param_type_checks(FILE *f){ TypeParamCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(!strcmp(up,"TEACH")){ char teachline[4096]; char *fname=NULL; char **params=NULL; int pcnt=0; char full_name[256]; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,¶ms,&pcnt); if(current_class[0]) snprintf(full_name,sizeof(full_name),"%s.%s",current_class,fname); else snprintf(full_name,sizeof(full_name),"%s",fname); collect_teach_param_checks_from_body(&checks,f,full_name,params,pcnt); } } rewind(f); return checks; } -static TypeFieldCheck *collect_class_field_type_checks(FILE *f){ TypeFieldCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(current_class[0]&&!strcmp(up,"HAS")){ const char *pcur=t+3; char *field=unquote_token(&pcur); char *type=unquote_token(&pcur); add_type_field_check(&checks,current_class,trim_inplace(field),trim_inplace(type)); free(field); free(type); } } rewind(f); return checks; } -static TypeMethodCheck *collect_class_method_type_checks(FILE *f){ TypeMethodCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(current_class[0]&&!strcmp(up,"TEACH")){ char teachline[4096]; char *fname=NULL; char **params=NULL; int pcnt=0; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,¶ms,&pcnt); add_type_method_check(&checks,current_class,trim_inplace(fname)); } } rewind(f); return checks; } -static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line[4096]; int line_no=0,errs=0; Var *types=NULL; TypeParamCheck *param_checks=NULL; TypeFieldCheck *field_checks=NULL; TypeMethodCheck *method_checks=NULL; if(!f){ fprintf(stderr,"Could not open %s\n",path); return 1; } param_checks=collect_function_param_type_checks(f); field_checks=collect_class_field_type_checks(f); method_checks=collect_class_method_type_checks(f); type_env_set(&types,"RESULT","ANY"); type_env_set(&types,"LASTERROR","TEXT"); type_env_set(&types,"LASTERRORFILE","TEXT"); type_env_set(&types,"LASTERRORLINE","NUMBER"); type_env_set(&types,"LASTEXIT","NUMBER"); while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; line_no++; strncpy(tmp,line,sizeof(line)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; parse_set_for_typecheck2(t,&name,&type,&expr); if(name&&*trim_inplace(name)){ char *nt=trim_inplace(name); char *tt=type?trim_inplace(type):NULL; got=simple_expr_type(types,expr); old=type_env_get(types,nt); if(tt&&*tt){ if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } type_env_set(&types,nt,tt); } else if(old){ if(got&&!type_words_match(old,got)){ if(strchr(nt,'.')) printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,old,got); else printf("%s:%d: Type mismatch for %s: it was first set as %s, but this value looks like %s.\n",path,line_no,nt,old,got); errs++; } } else if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *cls=objtype+7; if(!type_field_check_for(field_checks,cls,fieldname)){ if(got) printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,cls,fieldname,fieldname,got); else printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,cls,fieldname); errs++; } } else { printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before setting %s.\n",path,line_no,objname,objname,nt); errs++; } } else if(got) type_env_set(&types,nt,got); else type_env_set(&types,nt,"ANY"); } free(name); free(type); free(expr); } +static TypeArityCheck *collect_function_arity_checks(FILE *f){ TypeArityCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(!strcmp(up,"TEACH")){ char teachline[4096]; char *fname=NULL; char **params=NULL; int pcnt=0; char full_name[256]; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,¶ms,&pcnt); if(current_class[0]) snprintf(full_name,sizeof(full_name),"%s.%s",current_class,fname); else snprintf(full_name,sizeof(full_name),"%s",fname); add_type_arity_check(&checks,full_name,pcnt); while(fgets(line,sizeof(line),f)){ char inner[4096],iup[64],iw[128]; char *it; strncpy(inner,line,sizeof(inner)-1); inner[sizeof(inner)-1]=0; it=trim_inplace(inner); if(is_blank_or_comment(it)) continue; first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); if(!strcmp(iup,"END")||!strcmp(iup,"LEARNED")) break; } } } rewind(f); return checks; } +static TypeReturnCheck *collect_function_return_type_checks(FILE *f){ TypeReturnCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(!strcmp(up,"TEACH")){ char teachline[4096],full_name[256],*fname=NULL,**params=NULL; int pcnt=0; const char *returns; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; returns=find_word_ci(teachline,"RETURNS"); parse_teach_parts(teachline,&fname,¶ms,&pcnt); if(returns){ char ret[128],first_ret[128]; const char *p=returns+7; while(*p&&isspace((unsigned char)*p)) p++; snprintf(ret,sizeof(ret),"%s",p); if(sscanf(ret,"%127s",first_ret)==1){ if(current_class[0]) snprintf(full_name,sizeof(full_name),"%s.%s",current_class,fname); else snprintf(full_name,sizeof(full_name),"%s",fname); add_type_return_check(&checks,full_name,first_ret); } } while(fgets(line,sizeof(line),f)){ char inner[4096],iup[64],iw[128]; char *it; strncpy(inner,line,sizeof(inner)-1); inner[sizeof(inner)-1]=0; it=trim_inplace(inner); if(is_blank_or_comment(it)) continue; first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); if(!strcmp(iup,"END")||!strcmp(iup,"LEARNED")) break; } } } rewind(f); return checks; } +static TypeFieldCheck *collect_class_field_type_checks(FILE *f){ + TypeFieldCheck *checks=NULL; + char line[4096]; + char current_class[128]=""; + while(fgets(line,sizeof(line),f)){ + char tmp[4096],up[64],w[128]; + char *t; + strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; + t=trim_inplace(tmp); + if(is_blank_or_comment(t)) continue; + first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); + if(!strcmp(up,"CLASS")){ + const char *pcur=t+5; + char *cls=unquote_token(&pcur); + snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); + free(cls); + continue; + } + if(current_class[0]&&!strcmp(up,"TEACH")){ + while(fgets(line,sizeof(line),f)){ + char inner[4096],iup[64],iw[128]; + char *it; + strncpy(inner,line,sizeof(inner)-1); inner[sizeof(inner)-1]=0; + it=trim_inplace(inner); + if(is_blank_or_comment(it)) continue; + first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); + if(!strcmp(iup,"END")||!strcmp(iup,"LEARNED")) break; + } + continue; + } + if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } + if(current_class[0]&&!strcmp(up,"HAS")){ + const char *pcur=t+3; + char *field=unquote_token(&pcur); + char *type=unquote_token(&pcur); + add_type_field_check(&checks,current_class,trim_inplace(field),trim_inplace(type)); + free(field); free(type); + } + } + rewind(f); + return checks; +} +static TypeMethodCheck *collect_class_method_type_checks(FILE *f){ TypeMethodCheck *checks=NULL; char line[4096]; char current_class[128]=""; while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); snprintf(current_class,sizeof(current_class),"%s",trim_inplace(cls)); free(cls); continue; } if((!strcmp(up,"END")||!strcmp(up,"ENDCLASS"))&¤t_class[0]){ current_class[0]=0; continue; } if(current_class[0]&&!strcmp(up,"TEACH")){ char teachline[4096]; char *fname=NULL; char **params=NULL; int pcnt=0; strncpy(teachline,t,sizeof(teachline)-1); teachline[sizeof(teachline)-1]=0; parse_teach_parts(teachline,&fname,¶ms,&pcnt); add_type_method_check(&checks,current_class,trim_inplace(fname)); while(fgets(line,sizeof(line),f)){ char inner[4096],iup[64],iw[128]; char *it; strncpy(inner,line,sizeof(inner)-1); inner[sizeof(inner)-1]=0; it=trim_inplace(inner); if(is_blank_or_comment(it)) continue; first_word(it,iw,sizeof(iw)); upper_copy(iup,iw,sizeof(iup)); if(!strcmp(iup,"END")||!strcmp(iup,"LEARNED")) break; } } } rewind(f); return checks; } +static int typecheck_file(const char *path){ FILE *f=fopen(path,"rb"); char line[4096]; int line_no=0,errs=0; Var *types=NULL; TypeParamCheck *param_checks=NULL; TypeFieldCheck *field_checks=NULL; TypeMethodCheck *method_checks=NULL; TypeArityCheck *arity_checks=NULL; TypeReturnCheck *return_checks=NULL; char current_func[256]=""; char current_method[256]=""; char current_class[128]=""; char declared_classes[128][256]; char declared_functions[128][256]; char declared_methods[128][256]; char declared_fields[128][256]; char declared_objects[128][256]; int declared_class_count=0,declared_function_count=0,declared_method_count=0,declared_field_count=0,declared_object_count=0; int in_teach=0,return_seen=0,top_level_return_seen=0,nested_blocks=0,conditional_paths_complete=1; int branch_returned[64],branch_has_else[64]; if(!f){ fprintf(stderr,"Could not open %s\n",path); return 1; } param_checks=collect_function_param_type_checks(f); arity_checks=collect_function_arity_checks(f); return_checks=collect_function_return_type_checks(f); field_checks=collect_class_field_type_checks(f); method_checks=collect_class_method_type_checks(f); type_env_set(&types,"RESULT","ANY"); type_env_set(&types,"LASTERROR","TEXT"); type_env_set(&types,"LASTERRORFILE","TEXT"); type_env_set(&types,"LASTERRORLINE","NUMBER"); type_env_set(&types,"LASTEXIT","NUMBER"); while(fgets(line,sizeof(line),f)){ char tmp[4096],up[64],w[128]; char *t; line_no++; strncpy(tmp,line,sizeof(tmp)-1); tmp[sizeof(tmp)-1]=0; t=trim_inplace(tmp); if(is_blank_or_comment(t)) continue; first_word(t,w,sizeof(w)); upper_copy(up,w,sizeof(up)); if(!strcmp(up,"CLASS")){ const char *pcur=t+5; char *cls=unquote_token(&pcur); char *name=trim_inplace(cls); char *extra=trim_inplace((char *)pcur); int duplicate=0,ci; for(ci=0;cinext) if(ci_eq(fc->cls,current_class)) type_env_set(&types,fc->field,fc->type); } { const char *declared_return=type_return_check_for(return_checks,full_name); if(declared_return&&!claro_is_type_word(declared_return)){ printf("%s:%d: %s %s declares an unknown return type %s. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.\n",path,line_no,strchr(full_name,'.')?"Method":"Function",full_name,declared_return); errs++; } else if(declared_return) snprintf(current_func,sizeof(current_func),"%s",full_name); } } +else if(!strcmp(up,"IF")){ if(in_teach){ if(nested_blocks<64){ branch_returned[nested_blocks]=0; branch_has_else[nested_blocks]=0; } nested_blocks++; } } + else if(!strcmp(up,"ELSE")){ if(in_teach&&nested_blocks>0&&nested_blocks<=64){ if(!branch_returned[nested_blocks-1]) conditional_paths_complete=0; branch_has_else[nested_blocks-1]=1; branch_returned[nested_blocks-1]=0; } } + else if(!strcmp(up,"END")||!strcmp(up,"LEARNED")||!strcmp(up,"ENDIF")){ if(in_teach&&nested_blocks>0){ int nested_complete=(nested_blocks<=64&&branch_has_else[nested_blocks-1]&&branch_returned[nested_blocks-1]); if(nested_blocks<=64&&(!branch_has_else[nested_blocks-1]||!branch_returned[nested_blocks-1])) conditional_paths_complete=0; nested_blocks--; if(nested_complete&&nested_blocks>0&&nested_blocks<=64) branch_returned[nested_blocks-1]=1; } else if(in_teach){ if(current_func[0]&&!return_seen){ const char *need=type_return_check_for(return_checks,current_func); printf("%s:%d: %s %s declares RETURNS %s but has no RETURN statement. Add RETURN with a %s value.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } else if(current_func[0]&&(!top_level_return_seen&&!conditional_paths_complete)){ const char *need=type_return_check_for(return_checks,current_func); printf("%s:%d: %s %s declares RETURNS %s but does not return a %s value on every path. Add RETURN to each branch or after the conditional.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } in_teach=0; current_func[0]=0; current_method[0]=0; } else current_class[0]=0; } + else if(!strcmp(up,"RETURN")&¤t_func[0]){ const char *need=type_return_check_for(return_checks,current_func); char *return_expr=trim_inplace(t+6); const char *got=simple_expr_type(types,return_expr); return_seen=1; if(nested_blocks==0) top_level_return_seen=1; else if(nested_blocks<=64) branch_returned[nested_blocks-1]=1; if(need&&!*return_expr){ printf("%s:%d: %s %s needs a %s value after RETURN. Add a %s expression.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need,need); errs++; } else if(need&&!got){ char first_value[128]; if(return_value_extra_tokens(return_expr,first_value,sizeof(first_value))){ printf("%s:%d: %s %s has extra text after return value %s. Keep only one expression after RETURN.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,first_value); } else { printf("%s:%d: %s %s's return value could not be understood yet. Use a %s expression after RETURN.\n",path,line_no,strchr(current_func,'.')?"Method":"Function",current_func,need); } errs++; } else if(need&&got&&!type_words_match(need,got)){ char text_operator=(ci_eq(need,"NUMBER")&&ci_eq(got,"TEXT"))?text_operator_for_number_return(types,return_expr):0; const char *text_operand=text_operator?text_operand_for_operator(types,return_expr,text_operator):NULL; if(text_operand){ printf("%s:%d: Type mismatch for return from %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,current_func,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); } else { printf("%s:%d: Type mismatch for return from %s: expected %s, but this value looks like %s.\n",path,line_no,current_func,need,got); } errs++; } } + else if(!strcmp(up,"SET")){ char *name=NULL,*type=NULL,*expr=NULL; const char *got,*old; parse_set_for_typecheck2(t,&name,&type,&expr); if(name&&*trim_inplace(name)){ char *nt=trim_inplace(name); char *tt=type?trim_inplace(type):NULL; got=simple_expr_type(types,expr); if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')&&!type_env_get(types,nt)&&!type_field_check_for(field_checks,current_class,nt)){ if(got){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,current_class,nt,nt,got); } else { printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,current_class,nt); } errs++; free(name); free(type); free(expr); continue; } old=type_env_get(types,nt); if(tt&&*tt){ if(current_method[0]&¤t_class[0]&&!strchr(nt,'.')){ const char *declared=type_field_check_for(field_checks,current_class,nt); if(declared&&tt&&!claro_is_type_word(tt)){ printf("%s:%d: Field %s in %s needs a known type such as NUMBER, TEXT, or YESNO, but %s is not a Claro type. Use %s for %s.\n",path,line_no,nt,current_method,tt,declared,nt); errs++; got=NULL; } else if(declared&&tt&&!type_words_match(declared,tt)){ printf("%s:%d: Type mismatch for field %s in %s: class declares %s, but this assignment says %s. Use %s for %s.\n",path,line_no,nt,current_method,declared,tt,declared,nt); errs++; got=NULL; } else if(declared&&got&&!type_words_match(declared,got)){ printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } } if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *declared=type_field_check_for(field_checks,objtype+7,fieldname); if(declared&&got&&!type_words_match(declared,got)){ char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand){ printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); } else { printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,declared,got); } errs++; } else if(!declared){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,objtype+7,fieldname,fieldname,tt); errs++; } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } } else if(got&&!type_words_match(tt,got)){ printf("%s:%d: Type mismatch for %s: expected %s, but this value looks like %s.\n",path,line_no,nt,tt,got); errs++; } type_env_set(&types,nt,tt); } else if(old){ if(got&&!type_words_match(old,got)){ if(strchr(nt,'.')){ char text_operator=(ci_eq(old,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s: expected %s, but this value looks like %s.\n",path,line_no,nt,old,got); } else if(in_teach&¤t_class[0]&&type_field_check_for(field_checks,current_class,nt)){ const char *declared=type_field_check_for(field_checks,current_class,nt); char text_operator=(ci_eq(declared,"NUMBER")&&ci_eq(got,"TEXT"))?(text_operand_for_operator(types,expr,'+')?'+':(text_operand_for_operator(types,expr,'-')?'-':(text_operand_for_operator(types,expr,'*')?'*':(text_operand_for_operator(types,expr,'/')?'/':0)))):0; const char *text_operand=text_operator?text_operand_for_operator(types,expr,text_operator):NULL; if(text_operand) printf("%s:%d: Type mismatch for field %s in %s: %s needs NUMBER values, but %s looks like TEXT.\n",path,line_no,nt,current_method,text_operator=='+'?"addition":(text_operator=='-'?"subtraction":(text_operator=='*'?"multiplication":"division")),text_operand); else printf("%s:%d: Type mismatch for field %s in %s: expected %s, but this value looks like %s.\n",path,line_no,nt,current_method,declared,got); errs++; } else printf("%s:%d: Type mismatch for %s: it was first set as %s, but this value looks like %s.\n",path,line_no,nt,old,got); errs++; } } else if(strchr(nt,'.')){ char objname[128],fieldname[128]; char *dot=strchr(nt,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-nt),nt); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *cls=objtype+7; if(!type_field_check_for(field_checks,cls,fieldname)){ if(got&&!strchr(expr,'+')&&!strchr(expr,'-')&&!strchr(expr,'*')&&!strchr(expr,'/')) printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,cls,fieldname,fieldname,got); else printf("%s:%d: Object %s has no field %s. Check the field name or add the field to the class with the right type.\n",path,line_no,cls,fieldname); errs++; } else { const char *fieldtype=type_field_check_for(field_checks,cls,fieldname); if(got&&!type_words_match(fieldtype,got)){ printf("%s:%d: Type mismatch for field %s.%s: expected %s, but this value looks like %s.\n",path,line_no,objname,fieldname,fieldtype,got); errs++; } } } else { printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before setting %s.\n",path,line_no,objname,objname,nt); errs++; } } else if(got) type_env_set(&types,nt,got); else type_env_set(&types,nt,"ANY"); } free(name); free(type); free(expr); } else if(!strcmp(up,"ADD")){ const char *to=find_word_ci(t,"TO"); if(to){ char *expr=substr(t+3,to); char *name=xstrdup(trim_inplace((char*)to+2)); const char *listtype=type_env_get(types,name); const char *need=container_member_type(listtype,"LIST"); const char *got=simple_expr_type(types,expr); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for list %s: expected %s item, but this value looks like %s.\n",path,line_no,name,need,got); errs++; } free(expr); free(name); } } else if(!strcmp(up,"PUT")){ const char *key=find_word_ci(t,"KEY"), *val=find_word_ci(t,"VALUE"); if(key&&val){ char *name=substr(t+3,key); char *expr=xstrdup(trim_inplace((char*)val+5)); char *nt=trim_inplace(name); const char *maptype=type_env_get(types,nt); const char *need=container_member_type(maptype,"MAP"); const char *got=simple_expr_type(types,expr); if(need&&got&&!type_words_match(need,got)){ printf("%s:%d: Type mismatch for map %s: expected %s value, but this value looks like %s.\n",path,line_no,nt,need,got); errs++; } free(name); free(expr); } } else if(!strcmp(up,"ASK")){ const char *as=find_word_ci(t,"AS"); char *name=NULL,*type=NULL; if(as){ const char *pcur=as+2; name=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } else { const char *pcur=t+3; char *prompt=unquote_token(&pcur); free(prompt); name=unquote_token(&pcur); { char *rest=xstrdup(trim_inplace((char*)pcur)); if(claro_is_type_word(rest)) type=rest; else free(rest); } } if(name&&*trim_inplace(name)) type_env_set(&types,trim_inplace(name),type?type:"TEXT"); free(name); free(type); } - else if(!strcmp(up,"TYPE")&&starts_ci(t,"TYPE OF")){ char *name=check_var_after_as(t); if(name){ type_env_set(&types,name,"TEXT"); free(name); } } - else if(!strcmp(up,"NEW")){ const char *as=find_word_ci(t,"AS"); char *name=NULL; char *cls=NULL; char objtype[256]; if(as){ cls=substr(t+3,as); name=xstrdup(trim_inplace((char*)as+2)); } else { const char *pcur=t+3; cls=unquote_token(&pcur); name=unquote_token(&pcur); } snprintf(objtype,sizeof(objtype),"OBJECT:%s",trim_inplace(cls)); if(name&&*trim_inplace(name)){ TypeFieldCheck *fc; char fullname[256]; char *nt=trim_inplace(name); char *ct=trim_inplace(cls); type_env_set(&types,nt,objtype); for(fc=field_checks;fc;fc=fc->next){ if(ci_eq(fc->cls,ct)){ snprintf(fullname,sizeof(fullname),"%s.%s",nt,fc->field); type_env_set(&types,fullname,fc->type); } } } free(cls); free(name); } - else if((!strcmp(up,"DO")&&!find_word_ci(t,"TIMES"))||!strcmp(up,"CALL")){ char *fname=NULL; char **parts=NULL; int ac=0,i,is_method=0; char lookup[256],display[256]; if(!strcmp(up,"DO")){ const char *pcur=t+2; fname=unquote_token(&pcur); if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); } else { char *rest=t+4; const char *with=find_word_ci(rest,"WITH"); fname=with?substr(rest,with):xstrdup(rest); if(with) ac=split_args(with+4,&parts); } snprintf(lookup,sizeof(lookup),"%s",trim_inplace(fname)); snprintf(display,sizeof(display),"%s",lookup); { char *dot=strchr(lookup,'.'); if(dot){ char objname[128]; const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-lookup),lookup); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ char method[128]; const char *cls=objtype+7; snprintf(method,sizeof(method),"%s",dot+1); if(!type_method_check_for(method_checks,cls,method)){ printf("%s:%d: Object %s has no method %s. Check the method name or add TEACH %s inside CLASS %s.\n",path,line_no,cls,method,method,cls); errs++; } snprintf(lookup,sizeof(lookup),"%s.%s",cls,method); snprintf(display,sizeof(display),"%s",lookup); is_method=1; } - else if(!strcmp(up,"DO")){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before calling %s.\n",path,line_no,objname,objname,lookup); errs++; } } } for(i=0;i0&&!class_known){ printf("%s:%d: Class %s is not known yet. Check the class name or declare CLASS %s before creating %s.\n",path,line_no,ct,ct,nt); errs++; } for(oi=0;oinext){ if(ci_eq(fc->cls,ct)){ snprintf(fullname,sizeof(fullname),"%s.%s",nt,fc->field); type_env_set(&types,fullname,fc->type); } } } else if(cls&&*trim_inplace(cls)){ printf("%s:%d: NEW %s needs an object name. Add a name after the class, such as NEW %s player.\n",path,line_no,trim_inplace(cls),trim_inplace(cls)); errs++; } else { printf("%s:%d: NEW needs a class name. Add a class and object name, such as NEW Player player.\n",path,line_no); errs++; } free(cls); free(name); free(extra); } + else if((!strcmp(up,"DO")&&!find_word_ci(t,"TIMES"))||!strcmp(up,"CALL")){ char *fname=NULL; char **parts=NULL; int ac=0,i,is_method=0; char lookup[256],display[256]; if(!strcmp(up,"DO")){ const char *pcur=t+2; fname=unquote_token(&pcur); if(*trim_inplace((char*)pcur)) ac=split_args(pcur,&parts); } else { char *rest=t+4; const char *with=find_word_ci(rest,"WITH"); fname=with?substr(rest,with):xstrdup(rest); if(with&&*trim_inplace((char*)with+4)) ac=split_args(with+4,&parts); } snprintf(lookup,sizeof(lookup),"%s",trim_inplace(fname)); snprintf(display,sizeof(display),"%s",lookup); { char *dot=strchr(lookup,'.'); if(dot){ char objname[128]; const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-lookup),lookup); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ char method[128]; const char *cls=objtype+7; snprintf(method,sizeof(method),"%s",dot+1); if(!type_method_check_for(method_checks,cls,method)){ printf("%s:%d: Object %s has no method %s. Check the method name or add TEACH %s inside CLASS %s.\n",path,line_no,cls,method,method,cls); errs++; } snprintf(lookup,sizeof(lookup),"%s.%s",cls,method); snprintf(display,sizeof(display),"%s",lookup); is_method=1; } + else if(!objtype||(!starts_ci(objtype,"MODULE")&&(!strcmp(up,"DO")||type_method_name_exists(method_checks,dot+1)))){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before calling %s.\n",path,line_no,objname,objname,lookup); errs++; } } } for(i=0;ifunc,lookup)&&pc->pos>=ac){ printf("%s:%d: %s %s needs argument %s as %s, but this call does not provide it.\n",path,line_no,is_method?"Method":"Function",display,pc->param,pc->type); errs++; } pc=pc->next; } } { int expected=type_arity_check_for(arity_checks,lookup); if(expected>=0&&ac>expected){ printf("%s:%d: %s %s only accepts %d %s, but this call gives %d. Remove the extra %s.\n",path,line_no,is_method?"Method":"Function",display,expected,expected==1?"argument":"arguments",ac,ac-expected==1?"argument":"arguments"); errs++; } else if(expected>=0&&acfunc,lookup)&&mpc->pos>=ac){ has_checked_missing=1; break; } mpc=mpc->next; } if(!has_checked_missing){ printf("%s:%d: %s %s needs %d %s, but this call gives %d. Add the missing %s.\n",path,line_no,is_method?"Method":"Function",display,expected,expected==1?"argument":"arguments",ac,expected-ac==1?"argument":"arguments"); errs++; } } else if((!strcmp(up,"DO")||!strcmp(up,"CALL"))&&!is_method&&!strchr(lookup,'.')&&expected<0){ printf("%s:%d: Function %s is not known yet. Check the function name or add TEACH %s before calling it.\n",path,line_no,lookup,lookup); errs++; } } free(fname); } + else if(!strcmp(up,"CHECK")&&starts_ci(t,"CHECK TYPE")){ const char *is=find_word_ci(t,"IS"); if(is){ char *expr=substr(t+10,is); char *need=xstrdup(trim_inplace((char*)is+2)); char *et=trim_inplace(expr); if(current_func[0]&&!strchr(et,'.')){ TypeParamCheck *pc; for(pc=param_checks;pc;pc=pc->next){ if(ci_eq(pc->func,current_func)&&ci_eq(pc->param,et)){ type_env_set(&types,et,pc->type); break; } } } const char *got=simple_expr_type(types,et); char first_type[64]; int first_type_len=0; if(!*et){ printf("%s:%d: CHECK TYPE needs an expression before IS. Try: CHECK TYPE score IS NUMBER.\n",path,line_no); errs++; } else if(!*trim_inplace(need)){ printf("%s:%d: CHECK TYPE %s needs a type after IS. Try: CHECK TYPE %s IS NUMBER.\n",path,line_no,et,et); errs++; } else if(sscanf(need,"%63s%n",first_type,&first_type_len)==1 && claro_is_type_word(first_type) && *trim_inplace(need+first_type_len)){ printf("%s:%d: CHECK TYPE %s has extra text after type %s. Keep only the expression, IS, and one type.\n",path,line_no,et,first_type); errs++; } else if(!claro_is_type_word(need)){ printf("%s:%d: CHECK TYPE needs a known type such as NUMBER, TEXT, YESNO, LIST, or MAP, but %s is not a Claro type.\n",path,line_no,need); errs++; } else if(got&&!type_words_match(need,got)){ const char *method_field=(current_method[0]&¤t_class[0]&&!strchr(et,'.'))?type_field_check_for(field_checks,current_class,et):NULL; if(method_field){ printf("%s:%d: Type check failed in %s: expected %s, but %s looks like %s.\n",path,line_no,current_method,need,et,got); } else { printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,got); } errs++; } else if(!got&¤t_method[0]&¤t_class[0]&&!strchr(et,'.')&&!type_env_get(types,et)&&!type_field_check_for(field_checks,current_class,et)){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,current_class,et,et,need); errs++; } else if(!got&&strchr(et,'.')){ char objname[128],fieldname[128]; char *dot=strchr(et,'.'); const char *objtype; snprintf(objname,sizeof(objname),"%.*s",(int)(dot-et),et); snprintf(fieldname,sizeof(fieldname),"%s",dot+1); objtype=type_env_get(types,objname); if(objtype&&starts_ci(objtype,"OBJECT:")){ const char *declared=type_field_check_for(field_checks,objtype+7,fieldname); if(declared&&!type_words_match(need,declared)){ printf("%s:%d: Type check failed: expected %s, but %s looks like %s.\n",path,line_no,need,et,declared); errs++; } else if(!declared){ printf("%s:%d: Object %s has no field %s. Check the field name or add HAS %s %s to the class.\n",path,line_no,objtype+7,fieldname,fieldname,need); errs++; } } else if(!objtype){ printf("%s:%d: Object %s is not known yet. Create it with NEW ClassName %s before checking %s.\n",path,line_no,objname,objname,et); errs++; } } free(expr); free(need); } else if(!*trim_inplace(t+10)){ printf("%s:%d: CHECK TYPE needs an expression, IS, and a type. Try: CHECK TYPE score IS NUMBER.\n",path,line_no); errs++; } else { printf("%s:%d: CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.\n",path,line_no); errs++; } } } fclose(f); if(errs==0) printf("Type check OK\n"); return errs?1:0; } static char *read_file_text(const char *path){ FILE *f=fopen(path,"rb"); Str b; int c; if(!f) return NULL; str_init(&b); while((c=fgetc(f))!=EOF) str_ch(&b,(char)c); fclose(f); return str_take(&b); } @@ -571,29 +672,74 @@ static int run_tests(void){ int fails=0; printf("%s: %d failure(s)\n",fails?"FAIL":"PASS",fails); return fails?1:0; } static int write_text_file_simple(const char *path,const char *text){ FILE *f=fopen(path,"wb"); if(!f) return 0; fputs(text,f); fclose(f); return 1; } -static int create_new_project(const char *name){ char path[512]; char text[1024]; if(!name||!*name){ fprintf(stderr,"Project needs a name. Try: claro new MyProject\n"); return 1; } if(!make_folder(name)){ fprintf(stderr,"Could not create project folder: %s\n",name); return 1; } snprintf(path,sizeof(path),"%s/main.claro",name); write_text_file_simple(path,"SAY \"Welcome to Claro!\"\n\nSET name TO \"Learner\"\nSAY \"Hello \" + name\n\nTEACH greet person\n SAY \"Nice to meet you, \" + person\nEND\n\nDO greet \"Friend\"\n"); snprintf(path,sizeof(path),"%s/packages",name); make_folder(path); snprintf(path,sizeof(path),"%s/claro.project",name); snprintf(text,sizeof(text),"name: %s\nmain: main.claro\nversion: v1.18.26\npackages:\n",name); write_text_file_simple(path,text); snprintf(path,sizeof(path),"%s/claro.lock",name); write_text_file_simple(path,"# Claro package lock\nversion: v1.18.26\n"); snprintf(path,sizeof(path),"%s/README.md",name); write_text_file_simple(path,"# My Claro Project\n\nBuild Claro, then run this project with:\n\n```bash\nclaro run\n```\n\nUse packages with:\n\n```bash\nclaro package add text\nclaro package list\nclaro package doctor\n```\n"); printf("Created Claro project: %s\n",name); printf("Next steps:\n cd %s\n claro run\n",name); return 0; } +static int project_name_safe(const char *name){ int i; if(!name||!*name) return 0; if(strlen(name)>64) return 0; if(name[0]=='.'||name[0]=='-'||name[0]=='_') return 0; for(i=0;name[i];i++){ unsigned char c=(unsigned char)name[i]; if(!(isalnum(c)||c=='_'||c=='-')) return 0; } return 1; } +static int create_new_project(const char *name){ char path[512]; char text[1024]; if(!name||!*name){ fprintf(stderr,"Project needs a name. Try: claro new MyProject\n"); return 1; } if(strlen(name)>64){ fprintf(stderr,"Project names must be 64 characters or fewer.\n"); return 1; } if(!project_name_safe(name)){ fprintf(stderr,"Project names may use only letters, numbers, dash, and underscore.\n"); return 1; } if(!make_folder(name)){ fprintf(stderr,"Could not create project folder: %s\n",name); return 1; } snprintf(path,sizeof(path),"%s/main.claro",name); write_text_file_simple(path,"SAY \"Welcome to Claro!\"\n\nSET name TO \"Learner\"\nSAY \"Hello \" + name\n\nTEACH greet person\n SAY \"Nice to meet you, \" + person\nEND\n\nDO greet \"Friend\"\n"); snprintf(path,sizeof(path),"%s/packages",name); make_folder(path); snprintf(path,sizeof(path),"%s/claro.project",name); snprintf(text,sizeof(text),"manifest-version: 1\nname: %s\nmain: main.claro\nversion: v1.18.26\npackages:\n",name); write_text_file_simple(path,text); snprintf(path,sizeof(path),"%s/claro.lock",name); write_text_file_simple(path,"# Claro package lock\nlock-version: 1\nversion: v1.18.26\n"); snprintf(path,sizeof(path),"%s/README.md",name); write_text_file_simple(path,"# My Claro Project\n\nBuild Claro, then run this project with:\n\n```bash\nclaro run\n```\n\nUse packages with:\n\n```bash\nclaro package add text\nclaro package list\nclaro package doctor\n```\n"); printf("Created Claro project: %s\n",name); printf("Next steps:\n cd %s\n claro run\n",name); return 0; } static int run_repl(void){ Runtime rt; char line[4096]; Program p; rt_init(&rt); memset(&p,0,sizeof(p)); p.path=xstrdup(""); p.count=1; p.cap=1; p.lines=(char**)xmalloc(sizeof(char*)); printf("%s\n",CLARO_VERSION); printf("Type HELP for help, or EXIT to leave.\n"); while(1){ int pc=0; printf("> "); fflush(stdout); if(!fgets(line,sizeof(line),stdin)) break; { size_t n=strlen(line); while(n&&(line[n-1]=='\n'||line[n-1]=='\r')) line[--n]=0; } { char *t=trim_inplace(line); if(ci_eq(t,"EXIT")||ci_eq(t,"QUIT")) break; if(ci_eq(t,"HELP")){ printf("Try: SAY \"Hello\"\n"); printf("Try: SET name TO \"Jon\"\n"); printf("Try: SAY name\n"); continue; } if(!*t) continue; p.lines[0]=t; exec_line(&rt,&p,&pc,t); if(rt.error){ fprintf(stderr,"%s:%d: %s\n",rt.err_file?rt.err_file:"",rt.err_line,rt.err_msg?rt.err_msg:"error"); rt_clear_error(&rt); } } } return 0; } static int file_exists_simple(const char *path); static int path_exists_simple(const char *path){ struct stat st; return path&&*path&&stat(path,&st)==0; } -static int package_name_safe(const char *name){ int i; if(!name||!*name) return 0; if(name[0]=='.'||name[0]=='-'||name[0]=='_') return 0; for(i=0;name[i];i++){ unsigned char c=(unsigned char)name[i]; if(!(isalnum(c)||c=='_'||c=='-')) return 0; } return 1; } +static int package_name_too_long(const char *name){ return name&&strlen(name)>64; } +static int package_name_safe(const char *name){ int i; if(!name||!*name) return 0; if(package_name_too_long(name)) return 0; if(name[0]=='.'||name[0]=='-'||name[0]=='_') return 0; for(i=0;name[i];i++){ unsigned char c=(unsigned char)name[i]; if(!(isalnum(c)||c=='_'||c=='-')) return 0; } return 1; } static void package_checksum(const char *name,char *out,size_t n){ unsigned long h=5381; const char *p=name?name:""; while(*p){ h=((h<<5)+h)+(unsigned char)(*p++); } p="|local|1|claro"; while(*p){ h=((h<<5)+h)+(unsigned char)(*p++); } snprintf(out,n,"%08lx",h); } static void ensure_project_file(void){ if(!file_exists_simple("claro.project")) write_text_file_simple("claro.project","manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v1.18.26\npackages:\n"); } static char *project_value(const char *key){ char *txt=read_file_text("claro.project"); size_t kn=strlen(key); char *p,*out=NULL; if(!txt) return NULL; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,key,kn)==0 && t[kn]==':'){ out=xstrdup(trim_inplace(t+kn+1)); break; } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); return out; } static int package_has_name(const char *name){ char *txt=read_file_text("claro.project"); char *p; int found=0; if(!txt) return 0; p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(strcmp(pkg,name)==0){ found=1; break; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); return found; } -static int write_package_lock(void){ char *txt=read_file_text("claro.project"); FILE *f; char *p; if(!txt) return 0; f=fopen("claro.lock","wb"); if(!f){ free(txt); return 0; } fputs("# Claro package lock\nlock-version: 1\nversion: v1.18.26\n",f); p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ char sum[32]; package_checksum(pkg,sum,sizeof(sum)); fprintf(f,"package: %s\nchecksum: %s\n",pkg,sum); } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } fclose(f); free(txt); return 1; } -static int append_package_to_project(const char *name){ FILE *f; if(!package_name_safe(name)){ fprintf(stderr,"Package names may use only letters, numbers, dash, and underscore.\n"); return 0; } ensure_project_file(); if(package_has_name(name)){ printf("Package already listed: %s\n",name); return 1; } f=fopen("claro.project","ab"); if(!f) return 0; fprintf(f,"package: %s\n",name); fclose(f); write_package_lock(); return 1; } -static int remove_package_from_project(const char *name){ char *txt=read_file_text("claro.project"); FILE *f; char *p; int removed=0; if(!txt) return 0; f=fopen("claro.project","wb"); if(!f){ free(txt); return 0; } p=txt; while(*p){ char *line=p; char save; int skip=0; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *copy=xstrdup(line); char *t=trim_inplace(copy); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(strcmp(pkg,name)==0){ skip=1; removed=1; } } free(copy); } if(!skip) fprintf(f,"%s\n",line); if(save) p++; while(*p=='\n'||*p=='\r') p++; } fclose(f); free(txt); write_package_lock(); return removed; } -static void create_package_folder(const char *name){ char path[512], meta[512], readme[512], sum[32]; make_folder("packages"); snprintf(path,sizeof(path),"packages/%s",name); make_folder(path); snprintf(meta,sizeof(meta),"%s/claro.package",path); package_checksum(name,sum,sizeof(sum)); { char text[1024]; snprintf(text,sizeof(text),"manifest-version: 1\nname: %s\nversion: 1\nsource: local\nchecksum: %s\n",name,sum); write_text_file_simple(meta,text); } snprintf(readme,sizeof(readme),"%s/README.md",path); if(!file_exists_simple(readme)){ char text[512]; snprintf(text,sizeof(text),"# %s\n\nThis is a local Claro package folder.\n",name); write_text_file_simple(readme,text); } } -static int list_project_packages(void){ char *txt=read_file_text("claro.project"); char *p; int count=0; printf("Packages in claro.project:\n"); if(!txt){ printf(" (no claro.project yet)\n"); return 0; } p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ printf(" %s\n",pkg); count++; } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } if(!count) printf(" (none)\n"); free(txt); return 0; } -static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.project"); if(txt){ char *p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ char meta[512], *mt, sum[32]; snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&strstr(mt,"manifest-version: 1")&&strstr(mt,"checksum:")){ printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(strstr(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; } -static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); make_folder("packages"); write_package_lock(); printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1=17&&strncmp(line,"manifest-version:",17)==0){ const char *value=line+17; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==1&&value[0]=='1') matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } +static int package_project_name_valid(const char *text){ const char *p=text; int fields=0; int nonempty=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=5&&strncmp(line,"name:",5)==0){ const char *value=line+5; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if(value=5&&strncmp(line,"name:",5)==0){ const char *value=line+5; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,name,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } +static int package_manifest_version_value_matches(const char *text){ const char *p=text; int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=8&&strncmp(line,"version:",8)==0){ const char *value=line+8; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==1&&value[0]=='1') matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } +static int package_manifest_source_matches(const char *text){ const char *p=text; int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=7&&strncmp(line,"source:",7)==0){ const char *value=line+7; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==5&&strncmp(value,"local",5)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } +static int package_manifest_checksum_matches(const char *text,const char *expected){ const char *p=text; size_t n=strlen(expected); int fields=0; int matches=0; while(p&&*p){ const char *line=p; const char *end; while(*p&&*p!='\n'&&*p!='\r') p++; end=p; if((size_t)(end-line)>=9&&strncmp(line,"checksum:",9)==0){ const char *value=line+9; fields++; while(valuevalue&&isspace((unsigned char)end[-1])) end--; if((size_t)(end-value)==n&&strncmp(value,expected,n)==0) matches++; } while(*p=='\n'||*p=='\r') p++; } return fields==1&&matches==1; } +static int package_lock_version_matches(const char *text){ + const char *p=text; int fields=0, matches=0; + while(p&&*p){ + const char *line=p, *end; + while(*p&&*p!='\n'&&*p!='\r') p++; + end=p; + while(line=13&&strnicmp2(line,"lock-version:",13)==0){ + const char *value=line+13; fields++; + while(valuevalue&&isspace((unsigned char)end[-1])) end--; + if((size_t)(end-value)==1&&value[0]=='1') matches++; + } + while(*p=='\n'||*p=='\r') p++; + } + return fields==1&&matches==1; +} +static int package_lock_release_version_matches(const char *text){ + const char *p=text; int fields=0, matches=0; + while(p&&*p){ + const char *line=p, *end; + while(*p&&*p!='\n'&&*p!='\r') p++; + end=p; + while(line=8&&strncmp(line,"version:",8)==0){ + const char *value=line+8; fields++; + while(valuevalue&&isspace((unsigned char)end[-1])) end--; + if((size_t)(end-value)==8&&strncmp(value,"v1.18.26",8)==0) matches++; + } + while(*p=='\n'||*p=='\r') p++; + } + return fields==1&&matches==1; +} +static int package_doctor(void){ int ok=1; char *mainfile; char *txt; printf("%s\n",CLARO_VERSION); printf("Package/project doctor:\n"); printf(" %s claro.project\n",file_exists_simple("claro.project")?"OK":"MISSING"); if(!file_exists_simple("claro.project")) ok=0; mainfile=project_value("main"); if(mainfile&&*mainfile){ printf(" %s main script: %s\n",file_exists_simple(mainfile)?"OK":"MISSING",mainfile); if(!file_exists_simple(mainfile)) ok=0; } else { printf(" MISSING main setting\n"); ok=0; } printf(" %s packages folder\n",path_exists_simple("packages")?"OK":"MISSING"); if(!path_exists_simple("packages")) ok=0; printf(" %s claro.lock\n",file_exists_simple("claro.lock")?"OK":"MISSING"); if(!file_exists_simple("claro.lock")) ok=0; txt=read_file_text("claro.lock"); if(txt&&!package_lock_version_matches(txt)){ printf(" BAD lock version: expected exactly one lock-version: 1 line\n"); ok=0; } if(txt&&!package_lock_release_version_matches(txt)){ printf(" BAD lock release version: expected exactly one version: v1.18.26 line\n"); ok=0; } free(txt); txt=read_file_text("claro.project"); if(txt&&!project_package_names_safe()){ ok=0; } if(txt&& !package_manifest_version_matches(txt)){ printf(" BAD project manifest version: expected 1\n"); ok=0; } if(txt&& !package_project_name_valid(txt)){ printf(" BAD project manifest name: expected a non-empty name\n"); ok=0; } if(txt){ char *p=txt; while(*p){ char *line=p; char save; while(*p&&*p!='\n'&&*p!='\r') p++; save=*p; *p=0; { char *t=trim_inplace(line); if(strnicmp2(t,"package:",8)==0){ char *pkg=trim_inplace(t+8); if(*pkg){ char meta[512], *mt, sum[32]; if(!package_name_safe(pkg)){ printf(" BAD package name: %s\n",pkg); ok=0; } else { snprintf(meta,sizeof(meta),"packages/%s/claro.package",pkg); mt=read_file_text(meta); if(mt&&!package_manifest_version_value_matches(mt)){ printf(" BAD package version: %s\n",pkg); ok=0; } else if(mt&&!package_manifest_source_matches(mt)){ printf(" BAD package source: %s\n",pkg); ok=0; } else if(mt&&!package_manifest_version_matches(mt)){ printf(" BAD package manifest version: %s. Keep exactly one manifest-version: 1 line in packages/%s/claro.package.\n",pkg,pkg); ok=0; } else if(mt&&strstr(mt,"checksum:")){ if(!package_manifest_name_matches(mt,pkg)){ printf(" BAD package manifest name: %s\n",pkg); ok=0; } else { printf(" OK package manifest: %s\n",pkg); package_checksum(pkg,sum,sizeof(sum)); if(package_manifest_checksum_matches(mt,sum)) printf(" OK package checksum: %s\n",pkg); else { printf(" BAD package checksum: %s\n",pkg); ok=0; } if(package_lock_checksum_ok(pkg)) printf(" OK lock checksum: %s\n",pkg); else { printf(" BAD lock checksum: %s\n",pkg); ok=0; } } } else { printf(" MISSING package manifest: %s\n",pkg); ok=0; } free(mt); } } } } if(save) p++; while(*p=='\n'||*p=='\r') p++; } free(txt); } if(!lock_packages_match_project()) ok=0; free(mainfile); printf("%s\n",ok?"Package/project files look ready.":"Package/project files need attention."); return ok?0:1; } +static int run_package_cmd(int argc,char **argv,int arg){ if(arg>=argc){ printf("Package commands:\n claro package init\n claro package add NAME\n claro package remove NAME\n claro package list\n claro package doctor\n claro package lock\n"); return 0; } if(strcmp(argv[arg],"init")==0){ ensure_project_file(); if(!project_package_names_safe()) return 1; make_folder("packages"); if(!write_package_lock()) return 1; printf("Package project ready: claro.project, claro.lock, packages/\n"); return 0; } if(strcmp(argv[arg],"add")==0&&arg+1=argc) return 0; if(strcmp(argv[arg],"help")==0||strcmp(argv[arg],"--help")==0){ print_help(); return 0; } if(strcmp(argv[arg],"test")==0) return run_tests(); if(strcmp(argv[arg],"repl")==0) return run_repl(); if(strcmp(argv[arg],"examples")==0) return show_examples(); if(strcmp(argv[arg],"doctor")==0) return run_doctor(); if(strcmp(argv[arg],"validate")==0) return run_validate(); if(strcmp(argv[arg],"package")==0) return run_package_cmd(argc,argv,arg+1); if(strcmp(argv[arg],"ide")==0) return print_ide_info(); if(strcmp(argv[arg],"new")==0&&arg+1=argc){ Runtime rt; int rc; char *mainfile=project_value("main"); if(!mainfile||!*mainfile){ if(mainfile) free(mainfile); mainfile=xstrdup("main.claro"); } rt_init(&rt); rt.trace=trace; rt.script_argc=0; rt.script_argv=NULL; rc=run_file(&rt,mainfile); free(mainfile); return rc; } } { Runtime rt; int rc; rt_init(&rt); rt.trace=trace; rt.script_argc=argc-arg-1; rt.script_argv=argv+arg+1; rc=run_file(&rt,argv[arg]); return rc; } } diff --git a/tests/test_validate_ci_workflow.py b/tests/test_validate_ci_workflow.py new file mode 100644 index 0000000..f2c8306 --- /dev/null +++ b/tests/test_validate_ci_workflow.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Regression tests for the CI workflow release-gate validator.""" +import importlib.util +from pathlib import Path +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "validate_ci_workflow", ROOT / "tools" / "validate_ci_workflow.py" +) +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class ValidateCiWorkflowTests(unittest.TestCase): + def test_accepts_release_gate_commands_in_run_steps(self): + workflow = """ + steps: + - name: Build + run: make + - name: Release gates + run: ./claro validate + - name: Type diagnostics + run: python3 tools/validate_typecheck_diagnostics.py + - name: Version + run: python3 tools/validate_version_convention.py + - name: Packages + run: python3 tools/validate_package_security.py + - name: Compiler warnings + run: python3 tools/validate_compiler_warnings.py + - name: CI workflow + run: python3 tools/validate_ci_workflow.py + """ + self.assertEqual(MODULE.missing_release_gates(workflow), []) + + def test_requires_the_workflow_validator_to_run_in_ci(self): + self.assertIn("python3 tools/validate_ci_workflow.py", MODULE.REQUIRED_COMMANDS) + + def test_rejects_commands_that_only_appear_in_comments(self): + workflow = """ + steps: + # run: ./claro validate + # python3 tools/validate_typecheck_diagnostics.py + - name: Build + run: make + """ + self.assertEqual( + MODULE.missing_release_gates(workflow), MODULE.REQUIRED_COMMANDS + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_validate_ide_metadata.py b/tests/test_validate_ide_metadata.py new file mode 100644 index 0000000..896a600 --- /dev/null +++ b/tests/test_validate_ide_metadata.py @@ -0,0 +1,34 @@ +import json +import subprocess +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def test_ide_metadata_lists_current_typed_function_keywords(): + result = subprocess.run( + ["python3", "tools/validate_ide_metadata.py"], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + + assert result.returncode == 0, result.stdout + assert result.stdout.strip() == "IDE metadata validation OK" + + +def test_ide_metadata_is_valid_json_with_keyword_list(): + result = subprocess.run( + ["./claro", "ide"], + cwd=ROOT, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + check=True, + ) + metadata = json.loads(result.stdout) + + assert "RETURNS" in metadata["keywords"] + assert "CHECK" in metadata["keywords"] + assert "TYPE" in metadata["keywords"] diff --git a/tests/test_validate_typecheck_diagnostics.py b/tests/test_validate_typecheck_diagnostics.py new file mode 100644 index 0000000..7a5d1a8 --- /dev/null +++ b/tests/test_validate_typecheck_diagnostics.py @@ -0,0 +1,414 @@ +#!/usr/bin/env python3 +"""Regression tests for complete typecheck fixture coverage.""" +import importlib.util +from pathlib import Path +import subprocess +import unittest + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "validate_typecheck_diagnostics", ROOT / "tools" / "validate_typecheck_diagnostics.py" +) +MODULE = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(MODULE) + + +class ValidateTypecheckDiagnosticsTests(unittest.TestCase): + def test_lists_every_typecheck_fixture(self): + listed = set(MODULE.EXPECTED) | set(MODULE.EXPECTED_OK) + fixtures = { + str(path.relative_to(ROOT)) + for path in (ROOT / "tests").glob("typecheck_*.claro") + } + fixtures.add("tests/37_object_field_types.claro") + self.assertEqual(listed, fixtures) + + def test_includes_modern_chained_alias_method_success_fixture(self): + self.assertIn( + "tests/typecheck_object_alias_method_do_good.claro", + MODULE.EXPECTED_OK, + ) + def test_has_compatibility_chained_alias_method_extra_arg_fixture(self): + self.assertTrue( + (ROOT / "tests/typecheck_object_alias_method_call_extra_arg_bad.claro").exists() + ) + + def test_includes_dedicated_yesno_field_check_success_fixture(self): + self.assertIn( + "tests/typecheck_object_field_check_type_yesno_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_reverse_text_concatenation_success_fixture(self): + self.assertIn( + "tests/typecheck_object_field_text_concat_reverse_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_typed_function_return_fixtures(self): + self.assertIn("tests/typecheck_function_return_bad.claro", MODULE.EXPECTED) + self.assertIn("tests/typecheck_function_return_good.claro", MODULE.EXPECTED_OK) + + def test_includes_compatibility_missing_return_type_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_missing_return_type_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_function_return_arithmetic_success_fixture(self): + self.assertIn( + "tests/typecheck_function_return_multiplication_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_unknown_function_return_expression_fixtures(self): + self.assertIn( + "tests/typecheck_function_unknown_return_expression_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_function_unknown_return_expression_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_unknown_compatibility_function_return_expression_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_unknown_return_expression_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_missing_object_name_fixture(self): + self.assertIn( + "tests/typecheck_missing_object_name_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_missing_expected_check_type_fixture(self): + self.assertIn( + "tests/typecheck_missing_expected_type_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_missing_type_of_expression_fixture(self): + self.assertIn( + "tests/typecheck_missing_type_of_expression_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_lowercase_compatibility_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_lowercase_return_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_lowercase_compatibility_syntax_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_lowercase_return_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_missing_function_return_fixture(self): + self.assertIn("tests/typecheck_function_missing_return_bad.claro", MODULE.EXPECTED) + + def test_includes_missing_method_return_type_fixtures(self): + self.assertIn("tests/typecheck_method_missing_return_type_bad.claro", MODULE.EXPECTED) + self.assertIn("tests/typecheck_method_compat_missing_return_type_bad.claro", MODULE.EXPECTED) + + def test_includes_branch_missing_function_return_fixture(self): + self.assertIn("tests/typecheck_function_branch_missing_return_bad.claro", MODULE.EXPECTED) + + def test_includes_compatibility_branch_missing_function_return_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_branch_missing_return_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_branch_complete_function_return_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_branch_complete_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_empty_function_return_fixture(self): + self.assertIn("tests/typecheck_function_empty_return_bad.claro", MODULE.EXPECTED) + + def test_includes_extra_return_tokens_fixture(self): + self.assertIn( + "tests/typecheck_function_return_extra_tokens_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_function_extra_return_tokens_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_return_extra_tokens_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_function_empty_return_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_empty_return_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_empty_compatibility_method_return_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_empty_return_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_missing_method_return_fixture(self): + self.assertIn("tests/typecheck_method_missing_return_bad.claro", MODULE.EXPECTED) + + def test_includes_typed_method_return_expression_fixtures(self): + self.assertIn("tests/typecheck_method_return_expression_bad.claro", MODULE.EXPECTED) + self.assertIn("tests/typecheck_method_return_expression_good.claro", MODULE.EXPECTED_OK) + + def test_includes_unknown_method_return_expression_fixtures(self): + self.assertIn( + "tests/typecheck_method_unknown_return_expression_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_method_unknown_return_expression_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_method_field_return_diagnostic_fixture(self): + self.assertIn("tests/typecheck_method_field_return_bad.claro", MODULE.EXPECTED) + self.assertIn("tests/typecheck_method_field_return_good.claro", MODULE.EXPECTED_OK) + + def test_includes_compatibility_method_field_return_fixtures(self): + self.assertIn("tests/typecheck_method_compat_field_return_bad.claro", MODULE.EXPECTED) + self.assertIn("tests/typecheck_method_compat_field_return_good.claro", MODULE.EXPECTED_OK) + + def test_includes_compatibility_method_return_success_fixture(self): + self.assertIn("tests/typecheck_method_compat_return_good.claro", MODULE.EXPECTED_OK) + + def test_includes_compatibility_method_multiplication_return_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_multiplication_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_method_addition_return_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_addition_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_method_addition_return_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_addition_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_method_subtraction_return_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_subtraction_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_method_division_return_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_division_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_method_multiplication_return_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_return_multiplication_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_function_arithmetic_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_return_addition_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_function_subtraction_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_return_subtraction_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_function_division_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_return_division_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_function_multiplication_return_success_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_return_multiplication_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_function_missing_return_fixture(self): + self.assertIn( + "tests/typecheck_function_compat_missing_return_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_lowercase_compatibility_method_return_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_lowercase_return_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_missing_method_argument_fixture(self): + self.assertIn( + "tests/typecheck_method_call_missing_unchecked_arg_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_method_field_assignment_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_field_assignment_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_method_field_assignment_success_fixture(self): + self.assertIn( + "tests/typecheck_method_field_assignment_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_unknown_method_field_assignment_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_unknown_field_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_typed_method_unknown_field_diagnostics(self): + self.assertIn( + "tests/typecheck_method_unknown_field_typed_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_method_compat_unknown_field_typed_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_explicitly_typed_method_field_assignment_diagnostic(self): + self.assertIn( + "tests/typecheck_method_typed_field_assignment_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_method_text_field_assignment_diagnostic_fixture(self): + self.assertIn( + "tests/typecheck_method_text_field_assignment_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_method_text_field_assignment_success_fixture(self): + self.assertIn( + "tests/typecheck_method_text_field_assignment_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_method_text_concatenation_success_fixture(self): + self.assertIn( + "tests/typecheck_method_text_concat_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_method_text_concatenation_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_text_concat_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_yesno_method_field_assignment_diagnostics(self): + self.assertIn( + "tests/typecheck_method_yesno_field_assignment_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_method_compat_yesno_field_assignment_bad.claro", + MODULE.EXPECTED, + ) + + def test_includes_compatibility_yesno_method_field_assignment_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_yesno_field_assignment_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_number_method_field_assignment_success_fixture(self): + self.assertIn( + "tests/typecheck_method_compat_field_assignment_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_method_field_check_type_fixtures(self): + self.assertIn( + "tests/typecheck_method_field_check_type_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_method_field_check_type_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_compatibility_method_field_check_type_fixtures(self): + self.assertIn( + "tests/typecheck_method_compat_field_check_type_bad.claro", + MODULE.EXPECTED, + ) + self.assertIn( + "tests/typecheck_method_compat_field_check_type_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_includes_yesno_method_field_check_type_success_fixture(self): + self.assertIn( + "tests/typecheck_method_yesno_field_check_type_good.claro", + MODULE.EXPECTED_OK, + ) + + def test_release_validation_runs_method_field_diagnostic_fixtures(self): + source = (ROOT / "src" / "claro.c").read_text() + for fixture in ( + "tests/typecheck_method_field_assignment_bad.claro", + "tests/typecheck_method_text_field_assignment_bad.claro", + "tests/typecheck_method_compat_text_field_assignment_bad.claro", + "tests/typecheck_method_yesno_field_assignment_bad.claro", + "tests/typecheck_method_compat_yesno_field_assignment_bad.claro", + "tests/typecheck_method_field_check_type_bad.claro", + "tests/typecheck_method_compat_unknown_field_bad.claro", + "tests/typecheck_method_unknown_field_typed_bad.claro", + "tests/typecheck_method_compat_unknown_field_typed_bad.claro", + "tests/typecheck_duplicate_field_bad.claro", + ): + self.assertIn(f'"{fixture}"', source) + + def test_release_validation_runs_compatibility_function_missing_return_fixture(self): + source = (ROOT / "src" / "claro.c").read_text() + self.assertIn( + '"tests/typecheck_function_compat_missing_return_bad.claro"', + source, + ) + + def test_release_validation_runs_method_call_fixture(self): + result = subprocess.run( + [str(ROOT / "claro"), "validate"], + cwd=ROOT, + capture_output=True, + text=True, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + self.assertIn("tests/typecheck_method_call_bad.claro", result.stdout) + self.assertIn( + '"tests/typecheck_method_compat_lowercase_return_good.claro"', + (ROOT / "src" / "claro.c").read_text(), + ) + self.assertIn( + '"tests/typecheck_method_compat_return_multiplication_bad.claro"', + (ROOT / "src" / "claro.c").read_text(), + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/typecheck_duplicate_class_bad.claro b/tests/typecheck_duplicate_class_bad.claro new file mode 100644 index 0000000..1f3ea18 --- /dev/null +++ b/tests/typecheck_duplicate_class_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +CLASS Player + HAS name TEXT +END diff --git a/tests/typecheck_duplicate_field_bad.claro b/tests/typecheck_duplicate_field_bad.claro new file mode 100644 index 0000000..1cfbdde --- /dev/null +++ b/tests/typecheck_duplicate_field_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER + HAS score TEXT +END + +NEW Player player +SET player.score 10 diff --git a/tests/typecheck_duplicate_function_bad.claro b/tests/typecheck_duplicate_function_bad.claro new file mode 100644 index 0000000..f792e2e --- /dev/null +++ b/tests/typecheck_duplicate_function_bad.claro @@ -0,0 +1,7 @@ +TEACH greet name + SAY name +END + +TEACH greet person + SAY person +END \ No newline at end of file diff --git a/tests/typecheck_duplicate_object_bad.claro b/tests/typecheck_duplicate_object_bad.claro new file mode 100644 index 0000000..5bf29ee --- /dev/null +++ b/tests/typecheck_duplicate_object_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +NEW Player player diff --git a/tests/typecheck_extra_check_type_tokens_bad.claro b/tests/typecheck_extra_check_type_tokens_bad.claro new file mode 100644 index 0000000..7e15cc3 --- /dev/null +++ b/tests/typecheck_extra_check_type_tokens_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +CHECK TYPE score IS NUMBER TEXT diff --git a/tests/typecheck_extra_class_tokens_bad.claro b/tests/typecheck_extra_class_tokens_bad.claro new file mode 100644 index 0000000..c54dde4 --- /dev/null +++ b/tests/typecheck_extra_class_tokens_bad.claro @@ -0,0 +1,5 @@ +CLASS Player Extra + HAS score NUMBER +END + +NEW Player player diff --git a/tests/typecheck_extra_field_tokens_bad.claro b/tests/typecheck_extra_field_tokens_bad.claro new file mode 100644 index 0000000..395cde2 --- /dev/null +++ b/tests/typecheck_extra_field_tokens_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + HAS score NUMBER TEXT +END + +NEW Player player diff --git a/tests/typecheck_extra_new_tokens_bad.claro b/tests/typecheck_extra_new_tokens_bad.claro new file mode 100644 index 0000000..464c7e2 --- /dev/null +++ b/tests/typecheck_extra_new_tokens_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player extra \ No newline at end of file diff --git a/tests/typecheck_extra_teach_tokens_bad.claro b/tests/typecheck_extra_teach_tokens_bad.claro new file mode 100644 index 0000000..f0194ff --- /dev/null +++ b/tests/typecheck_extra_teach_tokens_bad.claro @@ -0,0 +1,3 @@ +TEACH greet RETURNS NUMBER extra + RETURN 1 +END diff --git a/tests/typecheck_extra_type_of_tokens_bad.claro b/tests/typecheck_extra_type_of_tokens_bad.claro new file mode 100644 index 0000000..1d83a72 --- /dev/null +++ b/tests/typecheck_extra_type_of_tokens_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +TYPE OF score AS kind extra diff --git a/tests/typecheck_function_branch_complete_good.claro b/tests/typecheck_function_branch_complete_good.claro new file mode 100644 index 0000000..1944133 --- /dev/null +++ b/tests/typecheck_function_branch_complete_good.claro @@ -0,0 +1,9 @@ +TEACH choose flag RETURNS NUMBER + IF flag + RETURN 1 + ELSE + RETURN 2 + END +END + +DO choose YES diff --git a/tests/typecheck_function_branch_missing_return_bad.claro b/tests/typecheck_function_branch_missing_return_bad.claro new file mode 100644 index 0000000..77d8e00 --- /dev/null +++ b/tests/typecheck_function_branch_missing_return_bad.claro @@ -0,0 +1,5 @@ +TEACH choose flag RETURNS NUMBER + IF flag + RETURN 1 + END +END diff --git a/tests/typecheck_function_call_missing_unchecked_arg_bad.claro b/tests/typecheck_function_call_missing_unchecked_arg_bad.claro new file mode 100644 index 0000000..a13b3d5 --- /dev/null +++ b/tests/typecheck_function_call_missing_unchecked_arg_bad.claro @@ -0,0 +1,5 @@ +TEACH greet name + SAY name +END + +CALL greet WITH diff --git a/tests/typecheck_function_call_unknown_bad.claro b/tests/typecheck_function_call_unknown_bad.claro new file mode 100644 index 0000000..662a49c --- /dev/null +++ b/tests/typecheck_function_call_unknown_bad.claro @@ -0,0 +1,6 @@ +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +CALL squre WITH 4 diff --git a/tests/typecheck_function_compat_branch_complete_good.claro b/tests/typecheck_function_compat_branch_complete_good.claro new file mode 100644 index 0000000..006a91e --- /dev/null +++ b/tests/typecheck_function_compat_branch_complete_good.claro @@ -0,0 +1,9 @@ +TEACH choose TAKES flag RETURNS NUMBER + IF flag + RETURN 1 + ELSE + RETURN 2 + END +LEARNED + +CALL choose WITH YES diff --git a/tests/typecheck_function_compat_branch_missing_return_bad.claro b/tests/typecheck_function_compat_branch_missing_return_bad.claro new file mode 100644 index 0000000..d9cb2d4 --- /dev/null +++ b/tests/typecheck_function_compat_branch_missing_return_bad.claro @@ -0,0 +1,5 @@ +TEACH choose TAKES flag RETURNS NUMBER + IF flag + RETURN 1 + END +LEARNED diff --git a/tests/typecheck_function_compat_duplicate_param_bad.claro b/tests/typecheck_function_compat_duplicate_param_bad.claro new file mode 100644 index 0000000..fec0af4 --- /dev/null +++ b/tests/typecheck_function_compat_duplicate_param_bad.claro @@ -0,0 +1,4 @@ +TEACH greet TAKES name, name + SAY name +LEARNED +CALL greet WITH "Ada", "Grace" diff --git a/tests/typecheck_function_compat_empty_return_bad.claro b/tests/typecheck_function_compat_empty_return_bad.claro new file mode 100644 index 0000000..eea43b4 --- /dev/null +++ b/tests/typecheck_function_compat_empty_return_bad.claro @@ -0,0 +1,4 @@ +TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN +LEARNED diff --git a/tests/typecheck_function_compat_lowercase_return_good.claro b/tests/typecheck_function_compat_lowercase_return_good.claro new file mode 100644 index 0000000..37c5fd1 --- /dev/null +++ b/tests/typecheck_function_compat_lowercase_return_good.claro @@ -0,0 +1,6 @@ +TEACH square TAKES amount returns NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount +LEARNED + +CALL square WITH 4 diff --git a/tests/typecheck_function_compat_missing_return_bad.claro b/tests/typecheck_function_compat_missing_return_bad.claro new file mode 100644 index 0000000..83881c8 --- /dev/null +++ b/tests/typecheck_function_compat_missing_return_bad.claro @@ -0,0 +1,3 @@ +TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER +LEARNED diff --git a/tests/typecheck_function_compat_missing_return_type_bad.claro b/tests/typecheck_function_compat_missing_return_type_bad.claro new file mode 100644 index 0000000..812d7b3 --- /dev/null +++ b/tests/typecheck_function_compat_missing_return_type_bad.claro @@ -0,0 +1,3 @@ +TEACH greet TAKES name RETURNS + RETURN name +LEARNED diff --git a/tests/typecheck_function_compat_return_addition_good.claro b/tests/typecheck_function_compat_return_addition_good.claro new file mode 100644 index 0000000..3551bda --- /dev/null +++ b/tests/typecheck_function_compat_return_addition_good.claro @@ -0,0 +1,6 @@ +TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 1 +LEARNED + +CALL total WITH 4 diff --git a/tests/typecheck_function_compat_return_bad.claro b/tests/typecheck_function_compat_return_bad.claro new file mode 100644 index 0000000..caa0ac4 --- /dev/null +++ b/tests/typecheck_function_compat_return_bad.claro @@ -0,0 +1,6 @@ +TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN "oops" +LEARNED + +CALL square WITH 4 diff --git a/tests/typecheck_function_compat_return_division_good.claro b/tests/typecheck_function_compat_return_division_good.claro new file mode 100644 index 0000000..5a4bf0d --- /dev/null +++ b/tests/typecheck_function_compat_return_division_good.claro @@ -0,0 +1,6 @@ +TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 +LEARNED + +CALL total WITH 4 diff --git a/tests/typecheck_function_compat_return_extra_tokens_bad.claro b/tests/typecheck_function_compat_return_extra_tokens_bad.claro new file mode 100644 index 0000000..5c69f38 --- /dev/null +++ b/tests/typecheck_function_compat_return_extra_tokens_bad.claro @@ -0,0 +1,3 @@ +TEACH square TAKES amount RETURNS NUMBER + RETURN amount extra +LEARNED diff --git a/tests/typecheck_function_compat_return_good.claro b/tests/typecheck_function_compat_return_good.claro new file mode 100644 index 0000000..d492852 --- /dev/null +++ b/tests/typecheck_function_compat_return_good.claro @@ -0,0 +1,6 @@ +TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount +LEARNED + +CALL square WITH 4 diff --git a/tests/typecheck_function_compat_return_multiplication_good.claro b/tests/typecheck_function_compat_return_multiplication_good.claro new file mode 100644 index 0000000..2410cba --- /dev/null +++ b/tests/typecheck_function_compat_return_multiplication_good.claro @@ -0,0 +1,6 @@ +TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * 2 +LEARNED + +CALL total WITH 4 diff --git a/tests/typecheck_function_compat_return_subtraction_good.claro b/tests/typecheck_function_compat_return_subtraction_good.claro new file mode 100644 index 0000000..1cb229c --- /dev/null +++ b/tests/typecheck_function_compat_return_subtraction_good.claro @@ -0,0 +1,6 @@ +TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 1 +LEARNED + +CALL total WITH 4 diff --git a/tests/typecheck_function_compat_unknown_return_expression_bad.claro b/tests/typecheck_function_compat_unknown_return_expression_bad.claro new file mode 100644 index 0000000..fa8d683 --- /dev/null +++ b/tests/typecheck_function_compat_unknown_return_expression_bad.claro @@ -0,0 +1,4 @@ +TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN missing +LEARNED diff --git a/tests/typecheck_function_duplicate_param_bad.claro b/tests/typecheck_function_duplicate_param_bad.claro new file mode 100644 index 0000000..2ba1409 --- /dev/null +++ b/tests/typecheck_function_duplicate_param_bad.claro @@ -0,0 +1,4 @@ +TEACH greet name, name + SAY name +END +DO greet "Ada", "Grace" diff --git a/tests/typecheck_function_empty_return_bad.claro b/tests/typecheck_function_empty_return_bad.claro new file mode 100644 index 0000000..d3e6567 --- /dev/null +++ b/tests/typecheck_function_empty_return_bad.claro @@ -0,0 +1,4 @@ +TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN +END diff --git a/tests/typecheck_function_extra_arg_bad.claro b/tests/typecheck_function_extra_arg_bad.claro new file mode 100644 index 0000000..f50bc05 --- /dev/null +++ b/tests/typecheck_function_extra_arg_bad.claro @@ -0,0 +1,6 @@ +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square 5, 6 diff --git a/tests/typecheck_function_lowercase_return_bad.claro b/tests/typecheck_function_lowercase_return_bad.claro new file mode 100644 index 0000000..38d1fdf --- /dev/null +++ b/tests/typecheck_function_lowercase_return_bad.claro @@ -0,0 +1,5 @@ +TEACH square amount returns NUMBER + RETURN "oops" +END + +DO square 4 \ No newline at end of file diff --git a/tests/typecheck_function_lowercase_return_good.claro b/tests/typecheck_function_lowercase_return_good.claro new file mode 100644 index 0000000..37c5fd1 --- /dev/null +++ b/tests/typecheck_function_lowercase_return_good.claro @@ -0,0 +1,6 @@ +TEACH square TAKES amount returns NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount +LEARNED + +CALL square WITH 4 diff --git a/tests/typecheck_function_missing_arg_bad.claro b/tests/typecheck_function_missing_arg_bad.claro new file mode 100644 index 0000000..6bb9883 --- /dev/null +++ b/tests/typecheck_function_missing_arg_bad.claro @@ -0,0 +1,6 @@ +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO square diff --git a/tests/typecheck_function_missing_return_bad.claro b/tests/typecheck_function_missing_return_bad.claro new file mode 100644 index 0000000..779dc82 --- /dev/null +++ b/tests/typecheck_function_missing_return_bad.claro @@ -0,0 +1,5 @@ +TEACH square amount RETURNS NUMBER + SAY amount +END + +DO square 4 diff --git a/tests/typecheck_function_missing_unchecked_arg_bad.claro b/tests/typecheck_function_missing_unchecked_arg_bad.claro new file mode 100644 index 0000000..3918b34 --- /dev/null +++ b/tests/typecheck_function_missing_unchecked_arg_bad.claro @@ -0,0 +1,5 @@ +TEACH greet name + SAY name +END + +DO greet diff --git a/tests/typecheck_function_nested_branch_complete_good.claro b/tests/typecheck_function_nested_branch_complete_good.claro new file mode 100644 index 0000000..d6fe983 --- /dev/null +++ b/tests/typecheck_function_nested_branch_complete_good.claro @@ -0,0 +1,13 @@ +TEACH choose flag RETURNS NUMBER + IF flag + IF flag + RETURN 1 + ELSE + RETURN 2 + END + ELSE + RETURN 3 + END +END + +DO choose YES diff --git a/tests/typecheck_function_return_arithmetic_bad.claro b/tests/typecheck_function_return_arithmetic_bad.claro new file mode 100644 index 0000000..a289930 --- /dev/null +++ b/tests/typecheck_function_return_arithmetic_bad.claro @@ -0,0 +1,4 @@ +TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + "oops" +END diff --git a/tests/typecheck_function_return_bad.claro b/tests/typecheck_function_return_bad.claro new file mode 100644 index 0000000..5897e8c --- /dev/null +++ b/tests/typecheck_function_return_bad.claro @@ -0,0 +1,6 @@ +TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN "oops" +END + +DO square 4 diff --git a/tests/typecheck_function_return_division_bad.claro b/tests/typecheck_function_return_division_bad.claro new file mode 100644 index 0000000..4262d43 --- /dev/null +++ b/tests/typecheck_function_return_division_bad.claro @@ -0,0 +1,4 @@ +TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / "oops" +END diff --git a/tests/typecheck_function_return_division_good.claro b/tests/typecheck_function_return_division_good.claro new file mode 100644 index 0000000..f0ac817 --- /dev/null +++ b/tests/typecheck_function_return_division_good.claro @@ -0,0 +1,6 @@ +TEACH halve amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 +END + +DO halve 8 diff --git a/tests/typecheck_function_return_expression_bad.claro b/tests/typecheck_function_return_expression_bad.claro new file mode 100644 index 0000000..ade8a3f --- /dev/null +++ b/tests/typecheck_function_return_expression_bad.claro @@ -0,0 +1,4 @@ +TEACH label amount RETURNS TEXT + CHECK TYPE amount IS NUMBER + RETURN amount + 1 +END diff --git a/tests/typecheck_function_return_expression_good.claro b/tests/typecheck_function_return_expression_good.claro new file mode 100644 index 0000000..89e2063 --- /dev/null +++ b/tests/typecheck_function_return_expression_good.claro @@ -0,0 +1,4 @@ +TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 1 +END diff --git a/tests/typecheck_function_return_extra_tokens_bad.claro b/tests/typecheck_function_return_extra_tokens_bad.claro new file mode 100644 index 0000000..932dd67 --- /dev/null +++ b/tests/typecheck_function_return_extra_tokens_bad.claro @@ -0,0 +1,3 @@ +TEACH square amount RETURNS NUMBER + RETURN amount extra +END diff --git a/tests/typecheck_function_return_good.claro b/tests/typecheck_function_return_good.claro new file mode 100644 index 0000000..7c3e78d --- /dev/null +++ b/tests/typecheck_function_return_good.claro @@ -0,0 +1,6 @@ +TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount +END + +DO square 4 diff --git a/tests/typecheck_function_return_multiplication_bad.claro b/tests/typecheck_function_return_multiplication_bad.claro new file mode 100644 index 0000000..43d3c23 --- /dev/null +++ b/tests/typecheck_function_return_multiplication_bad.claro @@ -0,0 +1,4 @@ +TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * "oops" +END diff --git a/tests/typecheck_function_return_multiplication_good.claro b/tests/typecheck_function_return_multiplication_good.claro new file mode 100644 index 0000000..a3ac133 --- /dev/null +++ b/tests/typecheck_function_return_multiplication_good.claro @@ -0,0 +1,6 @@ +TEACH scale amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * 2 +END + +DO scale 3 diff --git a/tests/typecheck_function_return_subtraction_bad.claro b/tests/typecheck_function_return_subtraction_bad.claro new file mode 100644 index 0000000..fd658dc --- /dev/null +++ b/tests/typecheck_function_return_subtraction_bad.claro @@ -0,0 +1,4 @@ +TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - "oops" +END diff --git a/tests/typecheck_function_return_subtraction_good.claro b/tests/typecheck_function_return_subtraction_good.claro new file mode 100644 index 0000000..e778995 --- /dev/null +++ b/tests/typecheck_function_return_subtraction_good.claro @@ -0,0 +1,6 @@ +TEACH difference amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 2 +END + +DO difference 5 diff --git a/tests/typecheck_function_unknown_bad.claro b/tests/typecheck_function_unknown_bad.claro new file mode 100644 index 0000000..c50deac --- /dev/null +++ b/tests/typecheck_function_unknown_bad.claro @@ -0,0 +1,6 @@ +TEACH square amount + CHECK TYPE amount IS NUMBER + SAY amount +END + +DO squre 4 diff --git a/tests/typecheck_function_unknown_return_expression_bad.claro b/tests/typecheck_function_unknown_return_expression_bad.claro new file mode 100644 index 0000000..3214425 --- /dev/null +++ b/tests/typecheck_function_unknown_return_expression_bad.claro @@ -0,0 +1,3 @@ +TEACH square amount RETURNS NUMBER + RETURN missing +END \ No newline at end of file diff --git a/tests/typecheck_function_unknown_return_expression_good.claro b/tests/typecheck_function_unknown_return_expression_good.claro new file mode 100644 index 0000000..73c213a --- /dev/null +++ b/tests/typecheck_function_unknown_return_expression_good.claro @@ -0,0 +1,4 @@ +TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount +END \ No newline at end of file diff --git a/tests/typecheck_invalid_expected_type_bad.claro b/tests/typecheck_invalid_expected_type_bad.claro new file mode 100644 index 0000000..bfaeb5f --- /dev/null +++ b/tests/typecheck_invalid_expected_type_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +CHECK TYPE score IS BANANA diff --git a/tests/typecheck_invalid_return_type_bad.claro b/tests/typecheck_invalid_return_type_bad.claro new file mode 100644 index 0000000..a5bfc89 --- /dev/null +++ b/tests/typecheck_invalid_return_type_bad.claro @@ -0,0 +1,3 @@ +TEACH square amount RETURNS BANANA + RETURN amount +END diff --git a/tests/typecheck_method_branch_missing_return_bad.claro b/tests/typecheck_method_branch_missing_return_bad.claro new file mode 100644 index 0000000..a4f9787 --- /dev/null +++ b/tests/typecheck_method_branch_missing_return_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + TEACH choose flag RETURNS NUMBER + IF flag + RETURN 1 + END + END +END diff --git a/tests/typecheck_method_call_bad.claro b/tests/typecheck_method_call_bad.claro new file mode 100644 index 0000000..00798d3 --- /dev/null +++ b/tests/typecheck_method_call_bad.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +CALL player.add WITH "five" diff --git a/tests/typecheck_method_call_good.claro b/tests/typecheck_method_call_good.claro new file mode 100644 index 0000000..9a2e23d --- /dev/null +++ b/tests/typecheck_method_call_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +CALL player.add WITH 5 diff --git a/tests/typecheck_method_call_missing_unchecked_arg_bad.claro b/tests/typecheck_method_call_missing_unchecked_arg_bad.claro new file mode 100644 index 0000000..777f77a --- /dev/null +++ b/tests/typecheck_method_call_missing_unchecked_arg_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS name TEXT + + TEACH rename newName + SET name newName + END +END + +NEW Player player +CALL player.rename WITH diff --git a/tests/typecheck_method_call_unknown_method_bad.claro b/tests/typecheck_method_call_unknown_method_bad.claro new file mode 100644 index 0000000..96b4757 --- /dev/null +++ b/tests/typecheck_method_call_unknown_method_bad.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +CALL player.fly WITH 5 diff --git a/tests/typecheck_method_call_unknown_object_bad.claro b/tests/typecheck_method_call_unknown_object_bad.claro new file mode 100644 index 0000000..a816bc5 --- /dev/null +++ b/tests/typecheck_method_call_unknown_object_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +CALL player.add WITH 5 diff --git a/tests/typecheck_method_call_unknown_object_unknown_method_bad.claro b/tests/typecheck_method_call_unknown_object_unknown_method_bad.claro new file mode 100644 index 0000000..c59b2b9 --- /dev/null +++ b/tests/typecheck_method_call_unknown_object_unknown_method_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +CALL player.fly WITH 5 diff --git a/tests/typecheck_method_compat_duplicate_name_bad.claro b/tests/typecheck_method_compat_duplicate_name_bad.claro new file mode 100644 index 0000000..3aae171 --- /dev/null +++ b/tests/typecheck_method_compat_duplicate_name_bad.claro @@ -0,0 +1,12 @@ +CLASS Player + TEACH show TAKES + SAY "first" + LEARNED + + TEACH show TAKES + SAY "second" + LEARNED +END + +NEW Player player +CALL player.show WITH diff --git a/tests/typecheck_method_compat_duplicate_param_bad.claro b/tests/typecheck_method_compat_duplicate_param_bad.claro new file mode 100644 index 0000000..0221770 --- /dev/null +++ b/tests/typecheck_method_compat_duplicate_param_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + TEACH greet TAKES name, name + SAY name + LEARNED +ENDCLASS + +NEW Player player +CALL player.greet WITH "Ada", "Grace" diff --git a/tests/typecheck_method_compat_empty_return_bad.claro b/tests/typecheck_method_compat_empty_return_bad.claro new file mode 100644 index 0000000..3000731 --- /dev/null +++ b/tests/typecheck_method_compat_empty_return_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + RETURN + LEARNED +ENDCLASS diff --git a/tests/typecheck_method_compat_field_assignment_good.claro b/tests/typecheck_method_compat_field_assignment_good.claro new file mode 100644 index 0000000..7abc6dd --- /dev/null +++ b/tests/typecheck_method_compat_field_assignment_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS score NUMBER + TEACH add TAKES amount + SET score amount + 1 + LEARNED +ENDCLASS + +NEW Player player +CALL player.add WITH 4 diff --git a/tests/typecheck_method_compat_field_check_type_bad.claro b/tests/typecheck_method_compat_field_check_type_bad.claro new file mode 100644 index 0000000..e67354b --- /dev/null +++ b/tests/typecheck_method_compat_field_check_type_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename TAKES replacement + CHECK TYPE name IS NUMBER + LEARNED +ENDCLASS + +NEW Player player +CALL player.rename WITH "Ada" diff --git a/tests/typecheck_method_compat_field_check_type_good.claro b/tests/typecheck_method_compat_field_check_type_good.claro new file mode 100644 index 0000000..e314eb1 --- /dev/null +++ b/tests/typecheck_method_compat_field_check_type_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename TAKES replacement + CHECK TYPE name IS TEXT + LEARNED +ENDCLASS + +NEW Player player +CALL player.rename WITH "Ada" diff --git a/tests/typecheck_method_compat_field_return_bad.claro b/tests/typecheck_method_compat_field_return_bad.claro new file mode 100644 index 0000000..4725351 --- /dev/null +++ b/tests/typecheck_method_compat_field_return_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER + + TEACH label TAKES amount RETURNS TEXT + RETURN score + LEARNED +ENDCLASS diff --git a/tests/typecheck_method_compat_field_return_good.claro b/tests/typecheck_method_compat_field_return_good.claro new file mode 100644 index 0000000..59a303a --- /dev/null +++ b/tests/typecheck_method_compat_field_return_good.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH score_value TAKES amount RETURNS NUMBER + RETURN score + LEARNED +ENDCLASS + +NEW Player player +CALL player.score_value WITH 4 diff --git a/tests/typecheck_method_compat_inline_field_annotation_bad.claro b/tests/typecheck_method_compat_inline_field_annotation_bad.claro new file mode 100644 index 0000000..baaff1c --- /dev/null +++ b/tests/typecheck_method_compat_inline_field_annotation_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score TAKES value + SET score TEXT 10 + LEARNED +END + +NEW Player player +CALL player.set_score WITH 10 diff --git a/tests/typecheck_method_compat_inline_field_annotation_good.claro b/tests/typecheck_method_compat_inline_field_annotation_good.claro new file mode 100644 index 0000000..dfcc9c0 --- /dev/null +++ b/tests/typecheck_method_compat_inline_field_annotation_good.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score TAKES value + SET score NUMBER 10 + LEARNED +END + +NEW Player player +CALL player.set_score WITH 10 diff --git a/tests/typecheck_method_compat_inline_field_annotation_unknown_type_bad.claro b/tests/typecheck_method_compat_inline_field_annotation_unknown_type_bad.claro new file mode 100644 index 0000000..9b9ab40 --- /dev/null +++ b/tests/typecheck_method_compat_inline_field_annotation_unknown_type_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score TAKES value + SET score AS BANANA TO 10 + LEARNED +END + +NEW Player player +CALL player.set_score WITH 10 diff --git a/tests/typecheck_method_compat_lowercase_return_bad.claro b/tests/typecheck_method_compat_lowercase_return_bad.claro new file mode 100644 index 0000000..a388133 --- /dev/null +++ b/tests/typecheck_method_compat_lowercase_return_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + TEACH score TAKES amount returns NUMBER + RETURN "oops" + LEARNED +END + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_lowercase_return_good.claro b/tests/typecheck_method_compat_lowercase_return_good.claro new file mode 100644 index 0000000..6365792 --- /dev/null +++ b/tests/typecheck_method_compat_lowercase_return_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount returns NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + LEARNED +END + +NEW Player player +CALL player.score WITH 4 \ No newline at end of file diff --git a/tests/typecheck_method_compat_missing_return_type_bad.claro b/tests/typecheck_method_compat_missing_return_type_bad.claro new file mode 100644 index 0000000..f5df706 --- /dev/null +++ b/tests/typecheck_method_compat_missing_return_type_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + TEACH score TAKES amount RETURNS + RETURN amount + LEARNED +END diff --git a/tests/typecheck_method_compat_return_addition_bad.claro b/tests/typecheck_method_compat_return_addition_bad.claro new file mode 100644 index 0000000..c2d5a44 --- /dev/null +++ b/tests/typecheck_method_compat_return_addition_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + "oops" + LEARNED +ENDCLASS diff --git a/tests/typecheck_method_compat_return_addition_good.claro b/tests/typecheck_method_compat_return_addition_good.claro new file mode 100644 index 0000000..02fe7df --- /dev/null +++ b/tests/typecheck_method_compat_return_addition_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 2 + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 \ No newline at end of file diff --git a/tests/typecheck_method_compat_return_bad.claro b/tests/typecheck_method_compat_return_bad.claro new file mode 100644 index 0000000..68797d7 --- /dev/null +++ b/tests/typecheck_method_compat_return_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN "oops" + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_return_division_bad.claro b/tests/typecheck_method_compat_return_division_bad.claro new file mode 100644 index 0000000..0647351 --- /dev/null +++ b/tests/typecheck_method_compat_return_division_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / "oops" + LEARNED +ENDCLASS diff --git a/tests/typecheck_method_compat_return_division_good.claro b/tests/typecheck_method_compat_return_division_good.claro new file mode 100644 index 0000000..3fa47b9 --- /dev/null +++ b/tests/typecheck_method_compat_return_division_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_return_extra_tokens_bad.claro b/tests/typecheck_method_compat_return_extra_tokens_bad.claro new file mode 100644 index 0000000..dddac36 --- /dev/null +++ b/tests/typecheck_method_compat_return_extra_tokens_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER + TEACH total TAKES amount RETURNS NUMBER + RETURN amount extra + LEARNED +END diff --git a/tests/typecheck_method_compat_return_good.claro b/tests/typecheck_method_compat_return_good.claro new file mode 100644 index 0000000..247db68 --- /dev/null +++ b/tests/typecheck_method_compat_return_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_return_multiplication_bad.claro b/tests/typecheck_method_compat_return_multiplication_bad.claro new file mode 100644 index 0000000..0f7b90f --- /dev/null +++ b/tests/typecheck_method_compat_return_multiplication_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * "oops" + LEARNED +ENDCLASS \ No newline at end of file diff --git a/tests/typecheck_method_compat_return_multiplication_good.claro b/tests/typecheck_method_compat_return_multiplication_good.claro new file mode 100644 index 0000000..3c706af --- /dev/null +++ b/tests/typecheck_method_compat_return_multiplication_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * 2 + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_return_subtraction_bad.claro b/tests/typecheck_method_compat_return_subtraction_bad.claro new file mode 100644 index 0000000..91fa6bb --- /dev/null +++ b/tests/typecheck_method_compat_return_subtraction_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH total TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - "oops" + LEARNED +ENDCLASS diff --git a/tests/typecheck_method_compat_return_subtraction_good.claro b/tests/typecheck_method_compat_return_subtraction_good.claro new file mode 100644 index 0000000..7bd212d --- /dev/null +++ b/tests/typecheck_method_compat_return_subtraction_good.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH score TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 2 + LEARNED +ENDCLASS + +NEW Player player +CALL player.score WITH 4 diff --git a/tests/typecheck_method_compat_text_concat_good.claro b/tests/typecheck_method_compat_text_concat_good.claro new file mode 100644 index 0000000..c39cef5 --- /dev/null +++ b/tests/typecheck_method_compat_text_concat_good.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS first TEXT + HAS last TEXT + + TEACH full_name TAKES suffix + SET first first + suffix + SET last "Dr. " + last + LEARNED +ENDCLASS + +NEW Player player +CALL player.full_name WITH "!" diff --git a/tests/typecheck_method_compat_text_field_assignment_bad.claro b/tests/typecheck_method_compat_text_field_assignment_bad.claro new file mode 100644 index 0000000..41da4e8 --- /dev/null +++ b/tests/typecheck_method_compat_text_field_assignment_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename TAKES replacement + SET name 123 + LEARNED +ENDCLASS + +NEW Player player +CALL player.rename WITH "Ada" \ No newline at end of file diff --git a/tests/typecheck_method_compat_text_field_assignment_good.claro b/tests/typecheck_method_compat_text_field_assignment_good.claro new file mode 100644 index 0000000..24509a9 --- /dev/null +++ b/tests/typecheck_method_compat_text_field_assignment_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename TAKES replacement + SET name replacement + LEARNED +ENDCLASS + +NEW Player player +CALL player.rename WITH "Ada" \ No newline at end of file diff --git a/tests/typecheck_method_compat_unknown_field_bad.claro b/tests/typecheck_method_compat_unknown_field_bad.claro new file mode 100644 index 0000000..b6349ca --- /dev/null +++ b/tests/typecheck_method_compat_unknown_field_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train TAKES value + SET level 3 + LEARNED +ENDCLASS + +NEW Player player +CALL player.train WITH 1 diff --git a/tests/typecheck_method_compat_unknown_field_check_type_bad.claro b/tests/typecheck_method_compat_unknown_field_check_type_bad.claro new file mode 100644 index 0000000..6690545 --- /dev/null +++ b/tests/typecheck_method_compat_unknown_field_check_type_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train TAKES value + CHECK TYPE level IS NUMBER + LEARNED +END + +NEW Player player +CALL player.train WITH 1 diff --git a/tests/typecheck_method_compat_unknown_field_expression_bad.claro b/tests/typecheck_method_compat_unknown_field_expression_bad.claro new file mode 100644 index 0000000..02ba636 --- /dev/null +++ b/tests/typecheck_method_compat_unknown_field_expression_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train TAKES value + SET level value + 1 + LEARNED +ENDCLASS + +NEW Player player +CALL player.train WITH 1 diff --git a/tests/typecheck_method_compat_unknown_field_typed_bad.claro b/tests/typecheck_method_compat_unknown_field_typed_bad.claro new file mode 100644 index 0000000..ac20413 --- /dev/null +++ b/tests/typecheck_method_compat_unknown_field_typed_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS score NUMBER + TEACH set_level TAKES nothing + SET level NUMBER 3 + LEARNED +ENDCLASS + +NEW Player player +CALL player.set_level WITH 0 diff --git a/tests/typecheck_method_compat_unknown_return_expression_bad.claro b/tests/typecheck_method_compat_unknown_return_expression_bad.claro new file mode 100644 index 0000000..c9aea3d --- /dev/null +++ b/tests/typecheck_method_compat_unknown_return_expression_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + TEACH square TAKES amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN missing + LEARNED +ENDCLASS + +NEW Player player +CALL player.square WITH 4 diff --git a/tests/typecheck_method_compat_yesno_field_assignment_bad.claro b/tests/typecheck_method_compat_yesno_field_assignment_bad.claro new file mode 100644 index 0000000..7f95de3 --- /dev/null +++ b/tests/typecheck_method_compat_yesno_field_assignment_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle TAKES value + SET ready "yes" + LEARNED +ENDCLASS + +NEW Player player +CALL player.toggle WITH YES diff --git a/tests/typecheck_method_compat_yesno_field_assignment_good.claro b/tests/typecheck_method_compat_yesno_field_assignment_good.claro new file mode 100644 index 0000000..0b82fc0 --- /dev/null +++ b/tests/typecheck_method_compat_yesno_field_assignment_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle TAKES value + SET ready value + LEARNED +ENDCLASS + +NEW Player player +CALL player.toggle WITH YES diff --git a/tests/typecheck_method_compat_yesno_field_check_type_bad.claro b/tests/typecheck_method_compat_yesno_field_check_type_bad.claro new file mode 100644 index 0000000..f90f7ae --- /dev/null +++ b/tests/typecheck_method_compat_yesno_field_check_type_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle TAKES replacement + CHECK TYPE ready IS TEXT + LEARNED +ENDCLASS + +NEW Player player +CALL player.toggle WITH YES diff --git a/tests/typecheck_method_compat_yesno_field_check_type_good.claro b/tests/typecheck_method_compat_yesno_field_check_type_good.claro new file mode 100644 index 0000000..92eba0a --- /dev/null +++ b/tests/typecheck_method_compat_yesno_field_check_type_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle TAKES replacement + CHECK TYPE ready IS YESNO + LEARNED +ENDCLASS + +NEW Player player +CALL player.toggle WITH YES diff --git a/tests/typecheck_method_duplicate_name_bad.claro b/tests/typecheck_method_duplicate_name_bad.claro new file mode 100644 index 0000000..562531c --- /dev/null +++ b/tests/typecheck_method_duplicate_name_bad.claro @@ -0,0 +1,12 @@ +CLASS Player + TEACH show + SAY "first" + END + + TEACH show + SAY "second" + END +END + +NEW Player player +DO player.show diff --git a/tests/typecheck_method_duplicate_param_bad.claro b/tests/typecheck_method_duplicate_param_bad.claro new file mode 100644 index 0000000..3908cfb --- /dev/null +++ b/tests/typecheck_method_duplicate_param_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + TEACH greet name, name + SAY name + END +END + +NEW Player player +DO player.greet "Ada", "Grace" diff --git a/tests/typecheck_method_extra_arg_bad.claro b/tests/typecheck_method_extra_arg_bad.claro new file mode 100644 index 0000000..72b6f01 --- /dev/null +++ b/tests/typecheck_method_extra_arg_bad.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +DO player.add 5, 6 diff --git a/tests/typecheck_method_field_assignment_bad.claro b/tests/typecheck_method_field_assignment_bad.claro new file mode 100644 index 0000000..341ea68 --- /dev/null +++ b/tests/typecheck_method_field_assignment_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + HAS name TEXT + TEACH add RETURNS NUMBER + SET score score + name + RETURN 1 + END +END +NEW Player player +DO player.add diff --git a/tests/typecheck_method_field_assignment_good.claro b/tests/typecheck_method_field_assignment_good.claro new file mode 100644 index 0000000..430227f --- /dev/null +++ b/tests/typecheck_method_field_assignment_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add RETURNS NUMBER + SET score score + 1 + RETURN score + END +END + +NEW Player player +DO player.add \ No newline at end of file diff --git a/tests/typecheck_method_field_check_type_bad.claro b/tests/typecheck_method_field_check_type_bad.claro new file mode 100644 index 0000000..b80aa2e --- /dev/null +++ b/tests/typecheck_method_field_check_type_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename replacement + CHECK TYPE name IS NUMBER + END +END + +NEW Player player +DO player.rename "Ada" diff --git a/tests/typecheck_method_field_check_type_good.claro b/tests/typecheck_method_field_check_type_good.claro new file mode 100644 index 0000000..20ea7b3 --- /dev/null +++ b/tests/typecheck_method_field_check_type_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename replacement + CHECK TYPE name IS TEXT + END +END + +NEW Player player +DO player.rename "Ada" diff --git a/tests/typecheck_method_field_return_bad.claro b/tests/typecheck_method_field_return_bad.claro new file mode 100644 index 0000000..24c8e40 --- /dev/null +++ b/tests/typecheck_method_field_return_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER + + TEACH label RETURNS TEXT + RETURN score + END +END diff --git a/tests/typecheck_method_field_return_good.claro b/tests/typecheck_method_field_return_good.claro new file mode 100644 index 0000000..2b4cfa6 --- /dev/null +++ b/tests/typecheck_method_field_return_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER + + TEACH score_value RETURNS NUMBER + RETURN score + END +END diff --git a/tests/typecheck_method_inline_field_annotation_bad.claro b/tests/typecheck_method_inline_field_annotation_bad.claro new file mode 100644 index 0000000..c798a1b --- /dev/null +++ b/tests/typecheck_method_inline_field_annotation_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score + SET score TEXT 10 + END +END + +NEW Player player +DO player.set_score diff --git a/tests/typecheck_method_inline_field_annotation_good.claro b/tests/typecheck_method_inline_field_annotation_good.claro new file mode 100644 index 0000000..3912051 --- /dev/null +++ b/tests/typecheck_method_inline_field_annotation_good.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score + SET score NUMBER 10 + END +END + +NEW Player player +DO player.set_score diff --git a/tests/typecheck_method_inline_field_annotation_unknown_type_bad.claro b/tests/typecheck_method_inline_field_annotation_unknown_type_bad.claro new file mode 100644 index 0000000..6be035c --- /dev/null +++ b/tests/typecheck_method_inline_field_annotation_unknown_type_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score + SET score AS BANANA TO 10 + END +END + +NEW Player player +DO player.set_score diff --git a/tests/typecheck_method_invalid_return_type_bad.claro b/tests/typecheck_method_invalid_return_type_bad.claro new file mode 100644 index 0000000..3d2f380 --- /dev/null +++ b/tests/typecheck_method_invalid_return_type_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + TEACH score amount RETURNS BANANA + RETURN amount + END +END diff --git a/tests/typecheck_method_missing_arg_bad.claro b/tests/typecheck_method_missing_arg_bad.claro new file mode 100644 index 0000000..f8c0e0a --- /dev/null +++ b/tests/typecheck_method_missing_arg_bad.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +DO player.add diff --git a/tests/typecheck_method_missing_return_bad.claro b/tests/typecheck_method_missing_return_bad.claro new file mode 100644 index 0000000..f61c623 --- /dev/null +++ b/tests/typecheck_method_missing_return_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + TEACH score amount RETURNS NUMBER + SAY amount + END +END diff --git a/tests/typecheck_method_missing_return_type_bad.claro b/tests/typecheck_method_missing_return_type_bad.claro new file mode 100644 index 0000000..5f4699c --- /dev/null +++ b/tests/typecheck_method_missing_return_type_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + TEACH score RETURNS + RETURN 1 + END +END diff --git a/tests/typecheck_method_missing_unchecked_arg_bad.claro b/tests/typecheck_method_missing_unchecked_arg_bad.claro new file mode 100644 index 0000000..83b3cc2 --- /dev/null +++ b/tests/typecheck_method_missing_unchecked_arg_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS name TEXT + + TEACH rename newName + SET name newName + END +END + +NEW Player player +DO player.rename diff --git a/tests/typecheck_method_nested_branch_complete_good.claro b/tests/typecheck_method_nested_branch_complete_good.claro new file mode 100644 index 0000000..236bca6 --- /dev/null +++ b/tests/typecheck_method_nested_branch_complete_good.claro @@ -0,0 +1,16 @@ +CLASS Player + TEACH choose flag RETURNS NUMBER + IF flag + IF flag + RETURN 1 + ELSE + RETURN 2 + END + ELSE + RETURN 3 + END + END +END + +NEW Player player +DO player.choose YES diff --git a/tests/typecheck_method_return_addition_bad.claro b/tests/typecheck_method_return_addition_bad.claro new file mode 100644 index 0000000..cceeccb --- /dev/null +++ b/tests/typecheck_method_return_addition_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + "oops" + END +END diff --git a/tests/typecheck_method_return_addition_good.claro b/tests/typecheck_method_return_addition_good.claro new file mode 100644 index 0000000..996fe70 --- /dev/null +++ b/tests/typecheck_method_return_addition_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 2 + END +END + +NEW Player player +DO player.total 3 \ No newline at end of file diff --git a/tests/typecheck_method_return_bad.claro b/tests/typecheck_method_return_bad.claro new file mode 100644 index 0000000..bfd321f --- /dev/null +++ b/tests/typecheck_method_return_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + TEACH score amount RETURNS NUMBER + RETURN "oops" + END +END + +NEW Player player +DO player.score 4 diff --git a/tests/typecheck_method_return_division_bad.claro b/tests/typecheck_method_return_division_bad.claro new file mode 100644 index 0000000..6659086 --- /dev/null +++ b/tests/typecheck_method_return_division_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / "oops" + END +END diff --git a/tests/typecheck_method_return_division_good.claro b/tests/typecheck_method_return_division_good.claro new file mode 100644 index 0000000..22be3a2 --- /dev/null +++ b/tests/typecheck_method_return_division_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount / 2 + END +END + +NEW Player player +DO player.total 4 diff --git a/tests/typecheck_method_return_expression_bad.claro b/tests/typecheck_method_return_expression_bad.claro new file mode 100644 index 0000000..82d71bf --- /dev/null +++ b/tests/typecheck_method_return_expression_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH label amount RETURNS TEXT + CHECK TYPE amount IS NUMBER + RETURN amount + 1 + END +END diff --git a/tests/typecheck_method_return_expression_good.claro b/tests/typecheck_method_return_expression_good.claro new file mode 100644 index 0000000..22ec4df --- /dev/null +++ b/tests/typecheck_method_return_expression_good.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH label amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + 1 + END +END diff --git a/tests/typecheck_method_return_extra_tokens_bad.claro b/tests/typecheck_method_return_extra_tokens_bad.claro new file mode 100644 index 0000000..40c913e --- /dev/null +++ b/tests/typecheck_method_return_extra_tokens_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER + TEACH total RETURNS NUMBER + RETURN 1 extra + END +END diff --git a/tests/typecheck_method_return_good.claro b/tests/typecheck_method_return_good.claro new file mode 100644 index 0000000..14c49dd --- /dev/null +++ b/tests/typecheck_method_return_good.claro @@ -0,0 +1,8 @@ +CLASS Player + TEACH score amount RETURNS NUMBER + RETURN amount + END +END + +NEW Player player +DO player.score 4 diff --git a/tests/typecheck_method_return_multiplication_bad.claro b/tests/typecheck_method_return_multiplication_bad.claro new file mode 100644 index 0000000..f3ce650 --- /dev/null +++ b/tests/typecheck_method_return_multiplication_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * "oops" + END +END diff --git a/tests/typecheck_method_return_multiplication_good.claro b/tests/typecheck_method_return_multiplication_good.claro new file mode 100644 index 0000000..148ed2f --- /dev/null +++ b/tests/typecheck_method_return_multiplication_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount * 2 + END +END + +NEW Player player +DO player.total 3 diff --git a/tests/typecheck_method_return_subtraction_bad.claro b/tests/typecheck_method_return_subtraction_bad.claro new file mode 100644 index 0000000..c5a02a2 --- /dev/null +++ b/tests/typecheck_method_return_subtraction_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - "oops" + END +END diff --git a/tests/typecheck_method_return_subtraction_good.claro b/tests/typecheck_method_return_subtraction_good.claro new file mode 100644 index 0000000..1af3042 --- /dev/null +++ b/tests/typecheck_method_return_subtraction_good.claro @@ -0,0 +1,11 @@ +CLASS Player + HAS score NUMBER + + TEACH total amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount - 2 + END +END + +NEW Player player +DO player.total 3 diff --git a/tests/typecheck_method_second_method_bad.claro b/tests/typecheck_method_second_method_bad.claro new file mode 100644 index 0000000..ca5fa1d --- /dev/null +++ b/tests/typecheck_method_second_method_bad.claro @@ -0,0 +1,15 @@ +CLASS Player + HAS score NUMBER + + TEACH show + SAY score + END + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +DO player.add "five" diff --git a/tests/typecheck_method_second_method_extra_arg_bad.claro b/tests/typecheck_method_second_method_extra_arg_bad.claro new file mode 100644 index 0000000..952ee76 --- /dev/null +++ b/tests/typecheck_method_second_method_extra_arg_bad.claro @@ -0,0 +1,15 @@ +CLASS Player + HAS score NUMBER + + TEACH show + SAY score + END + + TEACH add points + CHECK TYPE points IS NUMBER + SET score score + points + END +END + +NEW Player player +DO player.add 5, 6 diff --git a/tests/typecheck_method_text_concat_good.claro b/tests/typecheck_method_text_concat_good.claro new file mode 100644 index 0000000..85ab97c --- /dev/null +++ b/tests/typecheck_method_text_concat_good.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS first TEXT + HAS last TEXT + + TEACH full_name + SET first first + "!" + SET last "Dr. " + last + END +END + +NEW Player player +DO player.full_name diff --git a/tests/typecheck_method_text_field_assignment_bad.claro b/tests/typecheck_method_text_field_assignment_bad.claro new file mode 100644 index 0000000..e5524c8 --- /dev/null +++ b/tests/typecheck_method_text_field_assignment_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename + SET name 123 + END +END + +NEW Player player +DO player.rename \ No newline at end of file diff --git a/tests/typecheck_method_text_field_assignment_good.claro b/tests/typecheck_method_text_field_assignment_good.claro new file mode 100644 index 0000000..11e8571 --- /dev/null +++ b/tests/typecheck_method_text_field_assignment_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + TEACH rename + SET name "Ada" + END +END + +NEW Player player +DO player.rename \ No newline at end of file diff --git a/tests/typecheck_method_typed_field_assignment_bad.claro b/tests/typecheck_method_typed_field_assignment_bad.claro new file mode 100644 index 0000000..2bed153 --- /dev/null +++ b/tests/typecheck_method_typed_field_assignment_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH set_score + SET score TEXT "oops" + END +END + +NEW Player player +DO player.set_score \ No newline at end of file diff --git a/tests/typecheck_method_unknown_field_bad.claro b/tests/typecheck_method_unknown_field_bad.claro new file mode 100644 index 0000000..5475e1d --- /dev/null +++ b/tests/typecheck_method_unknown_field_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train + SET level 3 + END +END + +NEW Player player +DO player.train diff --git a/tests/typecheck_method_unknown_field_check_type_bad.claro b/tests/typecheck_method_unknown_field_check_type_bad.claro new file mode 100644 index 0000000..eeae402 --- /dev/null +++ b/tests/typecheck_method_unknown_field_check_type_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train + CHECK TYPE level IS NUMBER + END +END + +NEW Player player +DO player.train diff --git a/tests/typecheck_method_unknown_field_expression_bad.claro b/tests/typecheck_method_unknown_field_expression_bad.claro new file mode 100644 index 0000000..5d0c188 --- /dev/null +++ b/tests/typecheck_method_unknown_field_expression_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + HAS score NUMBER + + TEACH train + SET level value + 1 + END +END + +NEW Player player +DO player.train diff --git a/tests/typecheck_method_unknown_field_typed_bad.claro b/tests/typecheck_method_unknown_field_typed_bad.claro new file mode 100644 index 0000000..776c186 --- /dev/null +++ b/tests/typecheck_method_unknown_field_typed_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS score NUMBER + TEACH set_level + SET level NUMBER 3 + END +END + +NEW Player player +DO player.set_level diff --git a/tests/typecheck_method_unknown_return_expression_bad.claro b/tests/typecheck_method_unknown_return_expression_bad.claro new file mode 100644 index 0000000..898f4f8 --- /dev/null +++ b/tests/typecheck_method_unknown_return_expression_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN missing + END +END diff --git a/tests/typecheck_method_unknown_return_expression_good.claro b/tests/typecheck_method_unknown_return_expression_good.claro new file mode 100644 index 0000000..84fc299 --- /dev/null +++ b/tests/typecheck_method_unknown_return_expression_good.claro @@ -0,0 +1,6 @@ +CLASS Player + TEACH square amount RETURNS NUMBER + CHECK TYPE amount IS NUMBER + RETURN amount + END +END diff --git a/tests/typecheck_method_yesno_field_assignment_bad.claro b/tests/typecheck_method_yesno_field_assignment_bad.claro new file mode 100644 index 0000000..7adadf7 --- /dev/null +++ b/tests/typecheck_method_yesno_field_assignment_bad.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle + SET ready "yes" + END +END + +NEW Player player +DO player.toggle diff --git a/tests/typecheck_method_yesno_field_assignment_good.claro b/tests/typecheck_method_yesno_field_assignment_good.claro new file mode 100644 index 0000000..011dc8b --- /dev/null +++ b/tests/typecheck_method_yesno_field_assignment_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle value + SET ready value + END +END + +NEW Player player +DO player.toggle YES diff --git a/tests/typecheck_method_yesno_field_check_type_good.claro b/tests/typecheck_method_yesno_field_check_type_good.claro new file mode 100644 index 0000000..281c005 --- /dev/null +++ b/tests/typecheck_method_yesno_field_check_type_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS ready YESNO + TEACH toggle replacement + CHECK TYPE ready IS YESNO + END +END + +NEW Player player +DO player.toggle YES diff --git a/tests/typecheck_missing_class_name_bad.claro b/tests/typecheck_missing_class_name_bad.claro new file mode 100644 index 0000000..66363b6 --- /dev/null +++ b/tests/typecheck_missing_class_name_bad.claro @@ -0,0 +1,3 @@ +CLASS + HAS score NUMBER +END \ No newline at end of file diff --git a/tests/typecheck_missing_expected_type_bad.claro b/tests/typecheck_missing_expected_type_bad.claro new file mode 100644 index 0000000..c112807 --- /dev/null +++ b/tests/typecheck_missing_expected_type_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +CHECK TYPE score IS diff --git a/tests/typecheck_missing_expression_bad.claro b/tests/typecheck_missing_expression_bad.claro new file mode 100644 index 0000000..6350fea --- /dev/null +++ b/tests/typecheck_missing_expression_bad.claro @@ -0,0 +1 @@ +CHECK TYPE diff --git a/tests/typecheck_missing_expression_with_is_bad.claro b/tests/typecheck_missing_expression_with_is_bad.claro new file mode 100644 index 0000000..6ebb25b --- /dev/null +++ b/tests/typecheck_missing_expression_with_is_bad.claro @@ -0,0 +1 @@ +CHECK TYPE IS NUMBER diff --git a/tests/typecheck_missing_field_name_bad.claro b/tests/typecheck_missing_field_name_bad.claro new file mode 100644 index 0000000..d977b20 --- /dev/null +++ b/tests/typecheck_missing_field_name_bad.claro @@ -0,0 +1,3 @@ +CLASS Player + HAS +END diff --git a/tests/typecheck_missing_field_type_bad.claro b/tests/typecheck_missing_field_type_bad.claro new file mode 100644 index 0000000..ecbcd57 --- /dev/null +++ b/tests/typecheck_missing_field_type_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + HAS score +END + +NEW Player player diff --git a/tests/typecheck_missing_function_name_bad.claro b/tests/typecheck_missing_function_name_bad.claro new file mode 100644 index 0000000..7ab3d8d --- /dev/null +++ b/tests/typecheck_missing_function_name_bad.claro @@ -0,0 +1,3 @@ +TEACH + SAY "hello" +END diff --git a/tests/typecheck_missing_is_bad.claro b/tests/typecheck_missing_is_bad.claro new file mode 100644 index 0000000..54d92d3 --- /dev/null +++ b/tests/typecheck_missing_is_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +CHECK TYPE score diff --git a/tests/typecheck_missing_method_name_bad.claro b/tests/typecheck_missing_method_name_bad.claro new file mode 100644 index 0000000..44450bf --- /dev/null +++ b/tests/typecheck_missing_method_name_bad.claro @@ -0,0 +1,4 @@ +CLASS Player + TEACH + END +END diff --git a/tests/typecheck_missing_new_class_name_bad.claro b/tests/typecheck_missing_new_class_name_bad.claro new file mode 100644 index 0000000..03a0d6d --- /dev/null +++ b/tests/typecheck_missing_new_class_name_bad.claro @@ -0,0 +1 @@ +NEW diff --git a/tests/typecheck_missing_object_name_bad.claro b/tests/typecheck_missing_object_name_bad.claro new file mode 100644 index 0000000..1374110 --- /dev/null +++ b/tests/typecheck_missing_object_name_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player diff --git a/tests/typecheck_missing_return_type_bad.claro b/tests/typecheck_missing_return_type_bad.claro new file mode 100644 index 0000000..e87fd82 --- /dev/null +++ b/tests/typecheck_missing_return_type_bad.claro @@ -0,0 +1,3 @@ +TEACH greet RETURNS + RETURN 1 +END \ No newline at end of file diff --git a/tests/typecheck_missing_type_of_as_bad.claro b/tests/typecheck_missing_type_of_as_bad.claro new file mode 100644 index 0000000..e28121d --- /dev/null +++ b/tests/typecheck_missing_type_of_as_bad.claro @@ -0,0 +1,2 @@ +SET score NUMBER 10 +TYPE OF score diff --git a/tests/typecheck_missing_type_of_expression_bad.claro b/tests/typecheck_missing_type_of_expression_bad.claro new file mode 100644 index 0000000..6a037db --- /dev/null +++ b/tests/typecheck_missing_type_of_expression_bad.claro @@ -0,0 +1 @@ +TYPE OF AS kind diff --git a/tests/typecheck_nested_container_good.claro b/tests/typecheck_nested_container_good.claro new file mode 100644 index 0000000..5c82dbc --- /dev/null +++ b/tests/typecheck_nested_container_good.claro @@ -0,0 +1,4 @@ +SET people AS LIST OF MAP TO LIST +SET person AS MAP OF TEXT TO MAP +PUT person KEY "name" VALUE "Ada" +ADD person TO people diff --git a/tests/typecheck_object_alias_method_bad.claro b/tests/typecheck_object_alias_method_bad.claro new file mode 100644 index 0000000..6cdbd8b --- /dev/null +++ b/tests/typecheck_object_alias_method_bad.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + END +END + +NEW Player player +SET alias player +SET second alias +DO second.add "five" diff --git a/tests/typecheck_object_alias_method_call_bad.claro b/tests/typecheck_object_alias_method_call_bad.claro new file mode 100644 index 0000000..97d32d1 --- /dev/null +++ b/tests/typecheck_object_alias_method_call_bad.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + END +END + +NEW Player player +SET alias player +SET second alias +CALL second.add WITH "five" diff --git a/tests/typecheck_object_alias_method_call_extra_arg_bad.claro b/tests/typecheck_object_alias_method_call_extra_arg_bad.claro new file mode 100644 index 0000000..86238a6 --- /dev/null +++ b/tests/typecheck_object_alias_method_call_extra_arg_bad.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + END +END + +NEW Player player +SET alias player +SET second alias +CALL second.add WITH 5, 6 diff --git a/tests/typecheck_object_alias_method_call_good.claro b/tests/typecheck_object_alias_method_call_good.claro new file mode 100644 index 0000000..6f170ab --- /dev/null +++ b/tests/typecheck_object_alias_method_call_good.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + END +END + +NEW Player player +SET alias player +SET second alias +CALL second.add WITH 5 diff --git a/tests/typecheck_object_alias_method_do_good.claro b/tests/typecheck_object_alias_method_do_good.claro new file mode 100644 index 0000000..3df3404 --- /dev/null +++ b/tests/typecheck_object_alias_method_do_good.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS score NUMBER + + TEACH add points + CHECK TYPE points IS NUMBER + END +END + +NEW Player player +SET alias player +SET second alias +DO second.add 5 diff --git a/tests/typecheck_object_field_addition_bad.claro b/tests/typecheck_object_field_addition_bad.claro new file mode 100644 index 0000000..22cdc16 --- /dev/null +++ b/tests/typecheck_object_field_addition_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.name + 1 diff --git a/tests/typecheck_object_field_after_method_bad.claro b/tests/typecheck_object_field_after_method_bad.claro new file mode 100644 index 0000000..fe88c7a --- /dev/null +++ b/tests/typecheck_object_field_after_method_bad.claro @@ -0,0 +1,10 @@ +CLASS Player + TEACH show + SAY "player" + END + + HAS score NUMBER +END + +NEW Player player +SET player.score "ten" diff --git a/tests/typecheck_object_field_alias_bad.claro b/tests/typecheck_object_field_alias_bad.claro new file mode 100644 index 0000000..e88ff3e --- /dev/null +++ b/tests/typecheck_object_field_alias_bad.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET alias.score "ten" diff --git a/tests/typecheck_object_field_alias_good.claro b/tests/typecheck_object_field_alias_good.claro new file mode 100644 index 0000000..eecdba4 --- /dev/null +++ b/tests/typecheck_object_field_alias_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET alias.score 10 diff --git a/tests/typecheck_object_field_chained_alias_bad.claro b/tests/typecheck_object_field_chained_alias_bad.claro new file mode 100644 index 0000000..9bab070 --- /dev/null +++ b/tests/typecheck_object_field_chained_alias_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET backup alias +SET backup.score "ten" diff --git a/tests/typecheck_object_field_check_type_alias_bad.claro b/tests/typecheck_object_field_check_type_alias_bad.claro new file mode 100644 index 0000000..abf8852 --- /dev/null +++ b/tests/typecheck_object_field_check_type_alias_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET alias.score 10 +CHECK TYPE alias.score IS TEXT diff --git a/tests/typecheck_object_field_check_type_chained_alias_bad.claro b/tests/typecheck_object_field_check_type_chained_alias_bad.claro new file mode 100644 index 0000000..f76770c --- /dev/null +++ b/tests/typecheck_object_field_check_type_chained_alias_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET backup alias +CHECK TYPE backup.score IS TEXT diff --git a/tests/typecheck_object_field_check_type_chained_alias_good.claro b/tests/typecheck_object_field_check_type_chained_alias_good.claro new file mode 100644 index 0000000..f8159d7 --- /dev/null +++ b/tests/typecheck_object_field_check_type_chained_alias_good.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET alias player +SET backup alias +CHECK TYPE backup.score IS NUMBER diff --git a/tests/typecheck_object_field_check_type_chained_alias_text_good.claro b/tests/typecheck_object_field_check_type_chained_alias_text_good.claro new file mode 100644 index 0000000..22df2cc --- /dev/null +++ b/tests/typecheck_object_field_check_type_chained_alias_text_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT +END + +NEW Player player +SET alias player +SET backup alias +SET player.name "Ada" +CHECK TYPE backup.name IS TEXT diff --git a/tests/typecheck_object_field_check_type_yesno_good.claro b/tests/typecheck_object_field_check_type_yesno_good.claro new file mode 100644 index 0000000..87eb35b --- /dev/null +++ b/tests/typecheck_object_field_check_type_yesno_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS ready YESNO +END + +NEW Player player +SET player.ready YES +CHECK TYPE player.ready IS YESNO diff --git a/tests/typecheck_object_field_compound_bad.claro b/tests/typecheck_object_field_compound_bad.claro new file mode 100644 index 0000000..22cdc16 --- /dev/null +++ b/tests/typecheck_object_field_compound_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.name + 1 diff --git a/tests/typecheck_object_field_compound_good.claro b/tests/typecheck_object_field_compound_good.claro new file mode 100644 index 0000000..2b3b2d9 --- /dev/null +++ b/tests/typecheck_object_field_compound_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score 10 +SET player.score player.score + 1 diff --git a/tests/typecheck_object_field_division_bad.claro b/tests/typecheck_object_field_division_bad.claro new file mode 100644 index 0000000..166dfb5 --- /dev/null +++ b/tests/typecheck_object_field_division_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.score / player.name diff --git a/tests/typecheck_object_field_division_good.claro b/tests/typecheck_object_field_division_good.claro new file mode 100644 index 0000000..b36a58c --- /dev/null +++ b/tests/typecheck_object_field_division_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score 10 +SET player.score player.score / 2 diff --git a/tests/typecheck_object_field_expression_bad.claro b/tests/typecheck_object_field_expression_bad.claro new file mode 100644 index 0000000..0d04da0 --- /dev/null +++ b/tests/typecheck_object_field_expression_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.name diff --git a/tests/typecheck_object_field_expression_good.claro b/tests/typecheck_object_field_expression_good.claro new file mode 100644 index 0000000..a6f3761 --- /dev/null +++ b/tests/typecheck_object_field_expression_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score 10 +SET player.score player.score + 1 diff --git a/tests/typecheck_object_field_multiplication_bad.claro b/tests/typecheck_object_field_multiplication_bad.claro new file mode 100644 index 0000000..44bdbb4 --- /dev/null +++ b/tests/typecheck_object_field_multiplication_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.score * player.name \ No newline at end of file diff --git a/tests/typecheck_object_field_multiplication_good.claro b/tests/typecheck_object_field_multiplication_good.claro new file mode 100644 index 0000000..c67d10c --- /dev/null +++ b/tests/typecheck_object_field_multiplication_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score 10 +SET player.score player.score * 2 diff --git a/tests/typecheck_object_field_subtraction_bad.claro b/tests/typecheck_object_field_subtraction_bad.claro new file mode 100644 index 0000000..c5b33e6 --- /dev/null +++ b/tests/typecheck_object_field_subtraction_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.name "Ada" +SET player.score player.score - player.name diff --git a/tests/typecheck_object_field_subtraction_good.claro b/tests/typecheck_object_field_subtraction_good.claro new file mode 100644 index 0000000..9cccfd0 --- /dev/null +++ b/tests/typecheck_object_field_subtraction_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score 10 +SET player.score player.score - 2 diff --git a/tests/typecheck_object_field_text_concat_fields_good.claro b/tests/typecheck_object_field_text_concat_fields_good.claro new file mode 100644 index 0000000..698c770 --- /dev/null +++ b/tests/typecheck_object_field_text_concat_fields_good.claro @@ -0,0 +1,9 @@ +CLASS Player + HAS name TEXT + HAS nickname TEXT +END + +NEW Player player +SET player.name "Ada" +SET player.nickname "Lovelace" +SET player.name player.name + player.nickname diff --git a/tests/typecheck_object_field_text_concat_good.claro b/tests/typecheck_object_field_text_concat_good.claro new file mode 100644 index 0000000..939976b --- /dev/null +++ b/tests/typecheck_object_field_text_concat_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS name TEXT +END + +NEW Player player +SET player.name "Ada" +SET player.name player.name + " Lovelace" diff --git a/tests/typecheck_object_field_text_concat_reverse_good.claro b/tests/typecheck_object_field_text_concat_reverse_good.claro new file mode 100644 index 0000000..3be90a5 --- /dev/null +++ b/tests/typecheck_object_field_text_concat_reverse_good.claro @@ -0,0 +1,7 @@ +CLASS Player + HAS name TEXT +END + +NEW Player player +SET player.name "Lovelace" +SET player.name "Ada " + player.name diff --git a/tests/typecheck_object_field_text_expression_bad.claro b/tests/typecheck_object_field_text_expression_bad.claro new file mode 100644 index 0000000..69b527e --- /dev/null +++ b/tests/typecheck_object_field_text_expression_bad.claro @@ -0,0 +1,8 @@ +CLASS Player + HAS name TEXT + HAS score NUMBER +END + +NEW Player player +SET player.score 10 +SET player.name player.score + 1 diff --git a/tests/typecheck_object_field_typed_bad.claro b/tests/typecheck_object_field_typed_bad.claro new file mode 100644 index 0000000..b053949 --- /dev/null +++ b/tests/typecheck_object_field_typed_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.score TEXT "ten" diff --git a/tests/typecheck_object_field_unknown_typed_bad.claro b/tests/typecheck_object_field_unknown_typed_bad.claro new file mode 100644 index 0000000..67d089d --- /dev/null +++ b/tests/typecheck_object_field_unknown_typed_bad.claro @@ -0,0 +1,6 @@ +CLASS Player + HAS score NUMBER +END + +NEW Player player +SET player.level NUMBER 3 diff --git a/tests/typecheck_same_field_name_different_classes_good.claro b/tests/typecheck_same_field_name_different_classes_good.claro new file mode 100644 index 0000000..d6a5725 --- /dev/null +++ b/tests/typecheck_same_field_name_different_classes_good.claro @@ -0,0 +1,12 @@ +CLASS Player + HAS name TEXT +END + +CLASS Pet + HAS name TEXT +END + +NEW Player player +NEW Pet pet +SET player.name "Ada" +SET pet.name "Milo" diff --git a/tests/typecheck_unknown_class_bad.claro b/tests/typecheck_unknown_class_bad.claro new file mode 100644 index 0000000..81e06ce --- /dev/null +++ b/tests/typecheck_unknown_class_bad.claro @@ -0,0 +1,4 @@ +CLASS Player +END + +NEW Plaeyr player diff --git a/tests/typecheck_unknown_field_type_bad.claro b/tests/typecheck_unknown_field_type_bad.claro new file mode 100644 index 0000000..2ec9567 --- /dev/null +++ b/tests/typecheck_unknown_field_type_bad.claro @@ -0,0 +1,5 @@ +CLASS Player + HAS score BANANA +END + +NEW Player player diff --git a/tools/validate_ci_workflow.py b/tools/validate_ci_workflow.py new file mode 100644 index 0000000..a941736 --- /dev/null +++ b/tools/validate_ci_workflow.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +"""Validate that CI runs Claro's current release gates, not only legacy smoke tests.""" +from pathlib import Path +import sys + +ROOT = Path(__file__).resolve().parents[1] +WORKFLOW = ROOT / ".forgejo" / "workflows" / "ci.yml" + +REQUIRED_COMMANDS = [ + "./claro validate", + "python3 tools/validate_typecheck_diagnostics.py", + "python3 tools/validate_version_convention.py", + "python3 tools/validate_package_security.py", + "python3 tools/validate_compiler_warnings.py", + "python3 tools/validate_ci_workflow.py", +] + + +def _run_step_text(workflow_text: str) -> str: + """Return commands from YAML-like ``run`` steps, excluding comments/names.""" + commands = [] + in_run = False + run_indent = -1 + for raw_line in workflow_text.splitlines(): + stripped = raw_line.lstrip() + if not stripped or stripped.startswith("#"): + continue + indent = len(raw_line) - len(stripped) + if stripped.startswith("run:"): + in_run = True + run_indent = indent + commands.append(stripped[4:].strip()) + continue + if in_run: + if indent <= run_indent and stripped.startswith("-"): + in_run = False + elif indent <= run_indent and ":" in stripped: + in_run = False + else: + commands.append(stripped) + return "\n".join(commands) + + +def missing_release_gates(workflow_text: str): + run_text = _run_step_text(workflow_text) + return [command for command in REQUIRED_COMMANDS if command not in run_text] + + +def main() -> int: + if not WORKFLOW.exists(): + print(f"Missing CI workflow: {WORKFLOW.relative_to(ROOT)}") + return 1 + + text = WORKFLOW.read_text(encoding="utf-8") + missing = missing_release_gates(text) + if missing: + print("CI workflow is missing current release validation commands:") + for command in missing: + print(f" - {command}") + print("Add them to .forgejo/workflows/ci.yml so regressions are caught before release.") + return 1 + + print("CI workflow validation complete") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/validate_compiler_warnings.py b/tools/validate_compiler_warnings.py new file mode 100644 index 0000000..8934958 --- /dev/null +++ b/tools/validate_compiler_warnings.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +"""Keep the release build free of known path-truncation warnings.""" +import subprocess +import tempfile +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def main(): + with tempfile.TemporaryDirectory() as tmp: + result = subprocess.run( + ["gcc", "-std=c99", "-O0", "-c", "src/claro.c", "-o", str(Path(tmp) / "claro.o")], + cwd=ROOT, + text=True, + capture_output=True, + ) + if result.returncode != 0: + raise SystemExit(result.stderr or "compiler warning validation could not compile src/claro.c") + if "format-truncation" in result.stderr: + raise SystemExit("compiler warning validation found -Wformat-truncation warnings:\n" + result.stderr) + print("Compiler warning validation complete") + + +if __name__ == "__main__": + main() diff --git a/tools/validate_ide_metadata.py b/tools/validate_ide_metadata.py index 7096e2a..090d770 100644 --- a/tools/validate_ide_metadata.py +++ b/tools/validate_ide_metadata.py @@ -31,6 +31,9 @@ def main(): if ".claro" not in data["file_extensions"]: fail("IDE metadata must advertise the .claro file extension") + for keyword in ("RETURNS", "CHECK", "TYPE"): + if keyword not in data["keywords"]: + fail(f"IDE metadata must include the current typed-language keyword: {keyword}") if "LIST OF TEXT" not in data["types"]: fail("IDE metadata must include typed container types") if not any(cmd.get("name") == "typecheck" for cmd in data["commands"]): diff --git a/tools/validate_package_security.py b/tools/validate_package_security.py index 540648c..7a19cf6 100644 --- a/tools/validate_package_security.py +++ b/tools/validate_package_security.py @@ -31,7 +31,40 @@ def main(): fail(f"Expected {EXPECTED_VERSION}, got:\n{out}") with tempfile.TemporaryDirectory() as td: - work = Path(td) + root = Path(td) + work = root / "work" + work.mkdir() + + rc, out = run([str(EXE), "new", "../bad"], work) + if rc == 0: + fail("Unsafe project names with path separators must be rejected") + if "Project names may use only" not in out: + fail(f"Unsafe project-name diagnostic was unclear:\n{out}") + if (root / "bad").exists(): + fail("Unsafe project name should not create a folder outside the current project area") + + long_project_name = "P" * 65 + rc, out = run([str(EXE), "new", long_project_name], work) + if rc == 0: + fail("Project names longer than 64 characters must be rejected before paths are created") + if "Project names must be 64 characters or fewer" not in out: + fail(f"Long project-name diagnostic was unclear:\n{out}") + if (work / long_project_name).exists(): + fail("Long project name should not create a project folder") + + rc, out = run([str(EXE), "new", "StarterApp"], work) + if rc != 0: + fail(out) + starter = work / "StarterApp" + starter_project = read(starter / "claro.project") + for phrase in ["manifest-version: 1", "name: StarterApp", "main: main.claro", "version: v1.18.26", "packages:"]: + if phrase not in starter_project: + fail(f"claro new project file missing {phrase!r}:\n{starter_project}") + starter_lock = read(starter / "claro.lock") + for phrase in ["lock-version: 1", "version: v1.18.26"]: + if phrase not in starter_lock: + fail(f"claro new lockfile missing {phrase!r}:\n{starter_lock}") + (work / "main.claro").write_text('SAY "Package security demo"\n', encoding="utf-8") rc, out = run([str(EXE), "package", "init"], work) @@ -42,6 +75,17 @@ def main(): if phrase not in project: fail(f"claro.project missing {phrase!r}:\n{project}") + (work / "claro.project").write_text( + project.replace("manifest-version: 1", "manifest-version: 10", 1), + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a project manifest version that only starts with the supported version") + if "BAD project manifest version: expected 1" not in out: + fail(f"Package project manifest-version diagnostic was unclear:\n{out}") + (work / "claro.project").write_text(project, encoding="utf-8") + rc, out = run([str(EXE), "package", "add", "math-tools"], work) if rc != 0: fail(out) @@ -60,19 +104,305 @@ def main(): if phrase not in lock: fail(f"claro.lock missing {phrase!r}:\n{lock}") + for version_line in ["lock-version: 1", " LOCK-VERSION: \t1 \t"]: + valid_lock = lock.replace("lock-version: 1", version_line, 1) + (work / "claro.lock").write_text(valid_lock, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc != 0 or "Package/project files look ready." not in out: + fail(f"package doctor must accept a supported lock format version:\n{out}") + if read(work / "claro.lock") != valid_lock: + fail("package doctor must not rewrite a valid lockfile") + (work / "claro.lock").write_text(lock, encoding="utf-8") + + for version_lines in ["", "lock-version: 10", "lock-version:", + "lock-version: 1\nlock-version: 1", + "lock-version: 1\nlock-version: 10", + "lock-version: 10\nlock-version: 1"]: + invalid_lock = lock.replace("lock-version: 1", version_lines, 1) + (work / "claro.lock").write_text(invalid_lock, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail(f"package doctor must reject invalid lock format versions: {version_lines!r}") + if "BAD lock version: expected exactly one lock-version: 1 line" not in out: + fail(f"Lock format-version diagnostic was unclear:\n{out}") + if read(work / "claro.lock") != invalid_lock: + fail("package doctor must not rewrite an invalid lock format version") + invalid_release_lock = lock.replace("version: v1.18.26", "version: v1.18.25", 1) + (work / "claro.lock").write_text(invalid_release_lock, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a lockfile for a different Claro release") + if "BAD lock release version: expected exactly one version: v1.18.26 line" not in out: + fail(f"Lock release-version diagnostic was unclear:\n{out}") + if read(work / "claro.lock") != invalid_release_lock: + fail("package doctor must not rewrite an invalid lock release version") + (work / "claro.lock").write_text(lock, encoding="utf-8") + + (work / "claro.lock").write_text( + lock.replace("checksum:", "checksum: bad", 1), + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject stale lockfile checksums") + if "BAD lock checksum: math-tools" not in out: + fail(f"package doctor stale-lock diagnostic was unclear:\n{out}") + (work / "claro.lock").write_text(lock, encoding="utf-8") + + for sums in [(checksum, checksum), (checksum, "bad"), ("bad", checksum)]: + duplicate_lock = lock.replace( + "checksum: " + checksum, + "\n".join("checksum: " + value for value in sums), + 1, + ) + (work / "claro.lock").write_text(duplicate_lock, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail(f"package doctor must reject duplicate lock checksums: {sums}") + if "BAD lock checksum: math-tools" not in out: + fail(f"Duplicate lock checksum diagnostic was unclear:\n{out}") + if read(work / "claro.lock") != duplicate_lock: + fail("package doctor must not rewrite a lockfile with duplicate checksums") + (work / "claro.lock").write_text(lock, encoding="utf-8") + + checksum_line = next((line for line in manifest.splitlines() if line.startswith("checksum:")), "") + checksum = checksum_line.split(":", 1)[1].strip() + project_with_package = read(work / "claro.project") + manifest_path.write_text(manifest.replace("checksum: " + checksum, "checksum: " + checksum + "-extra"), encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a manifest checksum with trailing data") + if "BAD package checksum: math-tools" not in out: + fail(f"Package manifest checksum diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + for format_lines in ["", "manifest-version:", "manifest-version: 10", + "manifest-version: 1\nmanifest-version: 1", + "manifest-version: 1\nmanifest-version: 10", + "manifest-version: 10\nmanifest-version: 1"]: + invalid_manifest = manifest.replace("manifest-version: 1", format_lines, 1) + manifest_path.write_text(invalid_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail(f"package doctor must reject an invalid package manifest format: {format_lines!r}") + diagnostic = ("BAD package manifest version: math-tools. Keep exactly one " + "manifest-version: 1 line in packages/math-tools/claro.package.") + if diagnostic not in out or "MISSING package manifest: math-tools" in out: + fail(f"Package manifest-version diagnostic was unclear:\n{out}") + if read(manifest_path) != invalid_manifest: + fail("package doctor must not rewrite an invalid package manifest") + manifest_path.write_text(manifest, encoding="utf-8") + duplicate_version_manifest = manifest + "version: 1\n" + manifest_path.write_text(duplicate_version_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate package manifest versions") + if "BAD package version: math-tools" not in out: + fail(f"Package duplicate manifest-version diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + manifest_path.write_text(manifest.replace("version: 1", "version: 10"), encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a package version that only starts with the supported version") + if "BAD package version: math-tools" not in out: + fail(f"Package manifest version diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + + manifest_path.write_text(manifest.replace("source: local", "source: local-extra"), encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a package source that only starts with the supported source") + if "BAD package source: math-tools" not in out: + fail(f"Package manifest source diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + + for sources in [("local", "local"), ("local", "remote"), ("remote", "local")]: + duplicate_source_manifest = manifest.replace( + "source: local", "\n".join("source: " + source for source in sources), 1 + ) + manifest_path.write_text(duplicate_source_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail(f"package doctor must reject duplicate package manifest sources: {sources}") + if "BAD package source: math-tools" not in out: + fail(f"Package duplicate source diagnostic was unclear:\n{out}") + if read(manifest_path) != duplicate_source_manifest: + fail("package doctor must not rewrite a manifest with duplicate sources") + manifest_path.write_text(manifest, encoding="utf-8") + + duplicate_name_manifest = manifest + "name: other-tools\n" + manifest_path.write_text(duplicate_name_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate package manifest names") + if "BAD package manifest name: math-tools" not in out: + fail(f"Package duplicate manifest-name diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + + duplicate_checksum_manifest = manifest + "checksum: " + checksum + "\n" + manifest_path.write_text(duplicate_checksum_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate package manifest checksums") + if "BAD package checksum: math-tools" not in out: + fail(f"Package duplicate checksum diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) if rc != 0: fail(out) - for phrase in ["OK package manifest: math-tools", "OK package checksum: math-tools", "Package/project files look ready."]: + for phrase in ["OK package manifest: math-tools", "OK package checksum: math-tools", "OK lock checksum: math-tools", "Package/project files look ready."]: if phrase not in out: fail(f"package doctor output missing {phrase!r}:\n{out}") + manifest_path.unlink() + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a listed package whose manifest is missing") + if "MISSING package manifest: math-tools" not in out: + fail(f"package doctor missing-manifest diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest.replace("name: math-tools", "name: other-tools"), encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a manifest whose name does not match the project package") + if "BAD package manifest name: math-tools" not in out: + fail(f"package doctor manifest-name diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest.replace("name: math-tools", "name: math-tools-extra"), encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a manifest name that only starts with the project package name") + if "BAD package manifest name: math-tools" not in out: + fail(f"Package manifest prefix-match diagnostic was unclear:\n{out}") + manifest_path.write_text(manifest, encoding="utf-8") + + (work / "claro.lock").write_text( + lock + "package: ghost\nchecksum: 12345678\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject lockfile packages that are not listed in claro.project") + if "BAD lock package not in claro.project: ghost" not in out: + fail(f"package doctor orphan-lock diagnostic was unclear:\n{out}") + (work / "claro.lock").write_text(lock, encoding="utf-8") + + (work / "claro.lock").write_text( + lock + "package: math-tools\nchecksum: " + checksum + "\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate lockfile package entries") + if "DUPLICATE lock package: math-tools" not in out: + fail(f"package doctor duplicate-lock diagnostic was unclear:\n{out}") + (work / "claro.lock").write_text(lock, encoding="utf-8") + (work / "claro.project").write_text(project_with_package, encoding="utf-8") + + (work / "claro.project").write_text( + project_with_package.replace("name: ClaroProject", "name:", 1), + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject a project manifest with an empty name") + if "BAD project manifest name: expected a non-empty name" not in out: + fail(f"Package project-name diagnostic was unclear:\n{out}") + (work / "claro.project").write_text(project, encoding="utf-8") + duplicate_project_manifest = project_with_package + "manifest-version: 1\n" + (work / "claro.project").write_text(duplicate_project_manifest, encoding="utf-8") + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate project manifest versions") + if "BAD project manifest version: expected 1" not in out: + fail(f"Package duplicate project manifest-version diagnostic was unclear:\n{out}") + (work / "claro.project").write_text(project, encoding="utf-8") + + (work / "claro.project").write_text( + project + "package: math-tools\npackage: math-tools\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject duplicate package entries in claro.project") + if "DUPLICATE package: math-tools" not in out: + fail(f"Package duplicate diagnostic was unclear:\n{out}") + (work / "claro.project").write_text(project, encoding="utf-8") + rc, out = run([str(EXE), "package", "add", "../bad"], work) if rc == 0: fail("Unsafe package names with path separators must be rejected") if "Package names may use only" not in out: fail(f"Unsafe package diagnostic was unclear:\n{out}") + long_name = "a" * 65 + rc, out = run([str(EXE), "package", "add", long_name], work) + if rc == 0: + fail("Package names longer than 64 characters must be rejected before paths are created") + if "Package names must be 64 characters or fewer" not in out: + fail(f"Long package-name diagnostic was unclear:\n{out}") + if (work / "packages" / long_name).exists(): + fail("Long package name should not create a package folder") + + (work / "claro.project").write_text( + "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v1.18.26\npackages:\npackage: ../bad\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "doctor"], work) + if rc == 0: + fail("package doctor must reject unsafe package names already present in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package doctor unsafe-name diagnostic was unclear:\n{out}") + + rc, out = run([str(EXE), "package", "lock"], work) + if rc == 0: + fail("package lock must reject unsafe package names already present in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package lock unsafe-name diagnostic was unclear:\n{out}") + + rc, out = run([str(EXE), "package", "list"], work) + if rc == 0: + fail("package list must reject unsafe package names already present in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package list unsafe-name diagnostic was unclear:\n{out}") + + rc, out = run([str(EXE), "package", "init"], work) + if rc == 0: + fail("package init must not report success while unsafe package names remain in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package init existing unsafe-name diagnostic was unclear:\n{out}") + + rc, out = run([str(EXE), "package", "add", "science"], work) + if rc == 0: + fail("package add must not report success while unsafe package names remain in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package add existing unsafe-name diagnostic was unclear:\n{out}") + project_after_failed_add = read(work / "claro.project") + if "package: science" in project_after_failed_add: + fail("package add should not append a package while unsafe names remain in claro.project") + if (work / "packages" / "science").exists(): + fail("package add should not create a package folder while unsafe names remain in claro.project") + + (work / "claro.project").write_text( + "manifest-version: 1\nname: ClaroProject\nmain: main.claro\nversion: v1.18.26\npackages:\npackage: math-tools\npackage: ../bad\n", + encoding="utf-8", + ) + rc, out = run([str(EXE), "package", "remove", "math-tools"], work) + if rc == 0: + fail("package remove must not report success while unsafe package names remain in claro.project") + if "BAD package name: ../bad" not in out: + fail(f"package remove unsafe-name diagnostic was unclear:\n{out}") + + rc, out = run([str(EXE), "package", "remove", "../bad"], work) + if rc != 0: + fail(f"package remove should let learners remove an unsafe package entry from claro.project:\n{out}") + if "Removed package from project: ../bad" not in out: + fail(f"package remove unsafe-entry recovery output was unclear:\n{out}") + project_after_bad_remove = read(work / "claro.project") + if "package: ../bad" in project_after_bad_remove: + fail("package remove should remove the unsafe package entry from claro.project") + lock_after_bad_remove = read(work / "claro.lock") + if "package: ../bad" in lock_after_bad_remove: + fail("package remove should not keep unsafe package entries in claro.lock after recovery") + print("Package security validation OK") diff --git a/tools/validate_typecheck_diagnostics.py b/tools/validate_typecheck_diagnostics.py index 9e4a636..5cf2819 100644 --- a/tools/validate_typecheck_diagnostics.py +++ b/tools/validate_typecheck_diagnostics.py @@ -7,6 +7,54 @@ ROOT = Path(__file__).resolve().parents[1] EXE = ROOT / ("claro.exe" if os.name == "nt" else "claro") EXPECTED = { + "tests/typecheck_missing_type_of_expression_bad.claro": [ + "tests/typecheck_missing_type_of_expression_bad.claro:1: TYPE OF needs an expression before AS. Try: TYPE OF score AS kind.", + ], + "tests/typecheck_function_duplicate_param_bad.claro": [ + "tests/typecheck_function_duplicate_param_bad.claro:1: Function greet declares parameter name more than once. Give each parameter a different name.", + ], + "tests/typecheck_function_compat_duplicate_param_bad.claro": [ + "tests/typecheck_function_compat_duplicate_param_bad.claro:1: Function greet declares parameter name more than once. Give each parameter a different name.", + ], + "tests/typecheck_method_duplicate_param_bad.claro": [ + "tests/typecheck_method_duplicate_param_bad.claro:2: Method Player.greet declares parameter name more than once. Give each parameter a different name.", + ], + "tests/typecheck_method_compat_duplicate_param_bad.claro": [ + "tests/typecheck_method_compat_duplicate_param_bad.claro:2: Method Player.greet declares parameter name more than once. Give each parameter a different name.", + ], + "tests/typecheck_method_duplicate_name_bad.claro": [ + "tests/typecheck_method_duplicate_name_bad.claro:6: Method Player.show is declared more than once. Give each method a different name.", + ], + "tests/typecheck_method_compat_duplicate_name_bad.claro": [ + "tests/typecheck_method_compat_duplicate_name_bad.claro:6: Method Player.show is declared more than once. Give each method a different name.", + ], + "tests/typecheck_duplicate_field_bad.claro": [ + "tests/typecheck_duplicate_field_bad.claro:3: Class Player declares field score more than once. Give each field a different name.", + ], + "tests/typecheck_duplicate_class_bad.claro": [ + "tests/typecheck_duplicate_class_bad.claro:5: Class Player is declared more than once. Give each class a different name.", + ], + "tests/typecheck_duplicate_function_bad.claro": [ + "tests/typecheck_duplicate_function_bad.claro:5: Function greet is declared more than once. Give each function a different name.", + ], + "tests/typecheck_missing_function_name_bad.claro": [ + "tests/typecheck_missing_function_name_bad.claro:1: TEACH needs a function name. Add a name after TEACH, such as TEACH greet.", + ], + "tests/typecheck_missing_method_name_bad.claro": [ + "tests/typecheck_missing_method_name_bad.claro:2: Method Player needs a method name. Add a name after TEACH, such as TEACH show.", + ], + "tests/typecheck_missing_field_name_bad.claro": [ + "tests/typecheck_missing_field_name_bad.claro:2: Class Player needs a field name. Add a name and type after HAS, such as HAS score NUMBER.", + ], + "tests/typecheck_missing_class_name_bad.claro": [ + "tests/typecheck_missing_class_name_bad.claro:1: CLASS needs a class name. Add a name after CLASS, such as CLASS Player.", + ], + "tests/typecheck_duplicate_object_bad.claro": [ + "tests/typecheck_duplicate_object_bad.claro:6: Object player is created more than once. Give each object a different name.", + ], + "tests/typecheck_unknown_class_bad.claro": [ + "tests/typecheck_unknown_class_bad.claro:4: Class Plaeyr is not known yet. Check the class name or declare CLASS Plaeyr before creating player.", + ], "tests/typecheck_bad.claro": [ "tests/typecheck_bad.claro:2: Type mismatch for score: it was first set as NUMBER, but this value looks like TEXT.", "tests/typecheck_bad.claro:3: Type mismatch for name: expected TEXT, but this value looks like NUMBER.", @@ -19,22 +67,193 @@ EXPECTED = { "tests/typecheck_function_bad.claro": [ "tests/typecheck_function_bad.claro:5: Type mismatch for function square: parameter amount needs NUMBER, but this argument looks like TEXT.", ], + "tests/typecheck_function_return_bad.claro": [ + "tests/typecheck_function_return_bad.claro:3: Type mismatch for return from square: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_function_return_extra_tokens_bad.claro": [ + "tests/typecheck_function_return_extra_tokens_bad.claro:2: Function square has extra text after return value amount. Keep only one expression after RETURN.", + ], + "tests/typecheck_function_compat_return_extra_tokens_bad.claro": [ + "tests/typecheck_function_compat_return_extra_tokens_bad.claro:2: Function square has extra text after return value amount. Keep only one expression after RETURN.", + ], + "tests/typecheck_function_compat_empty_return_bad.claro": [ + "tests/typecheck_function_compat_empty_return_bad.claro:3: Function square needs a NUMBER value after RETURN. Add a NUMBER expression.", + ], + "tests/typecheck_method_return_extra_tokens_bad.claro": [ + "tests/typecheck_method_return_extra_tokens_bad.claro:4: Method Player.total has extra text after return value 1. Keep only one expression after RETURN.", + ], + "tests/typecheck_method_compat_return_extra_tokens_bad.claro": [ + "tests/typecheck_method_compat_return_extra_tokens_bad.claro:4: Method Player.total has extra text after return value amount. Keep only one expression after RETURN.", + ], + "tests/typecheck_function_lowercase_return_bad.claro": [ + "tests/typecheck_function_lowercase_return_bad.claro:2: Type mismatch for return from square: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_function_compat_return_bad.claro": [ + "tests/typecheck_function_compat_return_bad.claro:3: Type mismatch for return from square: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_function_return_expression_bad.claro": [ + "tests/typecheck_function_return_expression_bad.claro:3: Type mismatch for return from label: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_function_return_arithmetic_bad.claro": [ + "tests/typecheck_function_return_arithmetic_bad.claro:3: Type mismatch for return from total: addition needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_function_return_subtraction_bad.claro": [ + "tests/typecheck_function_return_subtraction_bad.claro:3: Type mismatch for return from total: subtraction needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_function_return_multiplication_bad.claro": [ + "tests/typecheck_function_return_multiplication_bad.claro:3: Type mismatch for return from total: multiplication needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_function_return_division_bad.claro": [ + "tests/typecheck_function_return_division_bad.claro:3: Type mismatch for return from total: division needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_return_subtraction_bad.claro": [ + "tests/typecheck_method_return_subtraction_bad.claro:6: Type mismatch for return from Player.total: subtraction needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_return_addition_bad.claro": [ + "tests/typecheck_method_return_addition_bad.claro:6: Type mismatch for return from Player.total: addition needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_return_division_bad.claro": [ + "tests/typecheck_method_return_division_bad.claro:6: Type mismatch for return from Player.total: division needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_return_multiplication_bad.claro": [ + "tests/typecheck_method_return_multiplication_bad.claro:6: Type mismatch for return from Player.total: multiplication needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_compat_return_addition_bad.claro": [ + "tests/typecheck_method_compat_return_addition_bad.claro:4: Type mismatch for return from Player.total: addition needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_compat_return_subtraction_bad.claro": [ + "tests/typecheck_method_compat_return_subtraction_bad.claro:4: Type mismatch for return from Player.total: subtraction needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_compat_return_division_bad.claro": [ + "tests/typecheck_method_compat_return_division_bad.claro:4: Type mismatch for return from Player.total: division needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_method_compat_return_multiplication_bad.claro": [ + "tests/typecheck_method_compat_return_multiplication_bad.claro:4: Type mismatch for return from Player.total: multiplication needs NUMBER values, but \"oops\" looks like TEXT.", + ], + "tests/typecheck_function_unknown_return_expression_bad.claro": [ + "tests/typecheck_function_unknown_return_expression_bad.claro:2: Function square's return value could not be understood yet. Use a NUMBER expression after RETURN.", + ], + "tests/typecheck_function_compat_unknown_return_expression_bad.claro": [ + "tests/typecheck_function_compat_unknown_return_expression_bad.claro:3: Function square's return value could not be understood yet. Use a NUMBER expression after RETURN.", + ], + "tests/typecheck_function_missing_return_bad.claro": [ + "tests/typecheck_function_missing_return_bad.claro:3: Function square declares RETURNS NUMBER but has no RETURN statement. Add RETURN with a NUMBER value.", + ], + "tests/typecheck_function_compat_missing_return_bad.claro": [ + "tests/typecheck_function_compat_missing_return_bad.claro:3: Function square declares RETURNS NUMBER but has no RETURN statement. Add RETURN with a NUMBER value.", + ], + "tests/typecheck_function_branch_missing_return_bad.claro": [ + "tests/typecheck_function_branch_missing_return_bad.claro:5: Function choose declares RETURNS NUMBER but does not return a NUMBER value on every path. Add RETURN to each branch or after the conditional.", + ], + "tests/typecheck_function_compat_branch_missing_return_bad.claro": [ + "tests/typecheck_function_compat_branch_missing_return_bad.claro:5: Function choose declares RETURNS NUMBER but does not return a NUMBER value on every path. Add RETURN to each branch or after the conditional.", + ], + "tests/typecheck_function_empty_return_bad.claro": [ + "tests/typecheck_function_empty_return_bad.claro:3: Function square needs a NUMBER value after RETURN. Add a NUMBER expression.", + ], + "tests/typecheck_method_return_bad.claro": [ + "tests/typecheck_method_return_bad.claro:3: Type mismatch for return from Player.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_method_compat_empty_return_bad.claro": [ + "tests/typecheck_method_compat_empty_return_bad.claro:3: Method Player.score needs a NUMBER value after RETURN. Add a NUMBER expression.", + ], + "tests/typecheck_method_compat_return_bad.claro": [ + "tests/typecheck_method_compat_return_bad.claro:4: Type mismatch for return from Player.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_method_compat_lowercase_return_bad.claro": [ + "tests/typecheck_method_compat_lowercase_return_bad.claro:3: Type mismatch for return from Player.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_method_return_expression_bad.claro": [ + "tests/typecheck_method_return_expression_bad.claro:4: Type mismatch for return from Player.label: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_method_unknown_return_expression_bad.claro": [ + "tests/typecheck_method_unknown_return_expression_bad.claro:4: Method Player.square's return value could not be understood yet. Use a NUMBER expression after RETURN.", + ], + "tests/typecheck_method_compat_unknown_return_expression_bad.claro": [ + "tests/typecheck_method_compat_unknown_return_expression_bad.claro:4: Method Player.square's return value could not be understood yet. Use a NUMBER expression after RETURN.", + ], + "tests/typecheck_method_field_return_bad.claro": [ + "tests/typecheck_method_field_return_bad.claro:5: Type mismatch for return from Player.label: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_method_compat_field_return_bad.claro": [ + "tests/typecheck_method_compat_field_return_bad.claro:5: Type mismatch for return from Player.label: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_method_missing_return_bad.claro": [ + "tests/typecheck_method_missing_return_bad.claro:4: Method Player.score declares RETURNS NUMBER but has no RETURN statement. Add RETURN with a NUMBER value.", + ], + "tests/typecheck_method_branch_missing_return_bad.claro": [ + "tests/typecheck_method_branch_missing_return_bad.claro:6: Method Player.choose declares RETURNS NUMBER but does not return a NUMBER value on every path. Add RETURN to each branch or after the conditional.", + ], "tests/typecheck_function_multi_bad.claro": [ "tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter name needs TEXT, but this argument looks like NUMBER.", "tests/typecheck_function_multi_bad.claro:8: Type mismatch for function label: parameter age needs NUMBER, but this argument looks like TEXT.", ], + "tests/typecheck_function_missing_arg_bad.claro": [ + "tests/typecheck_function_missing_arg_bad.claro:6: Function square needs argument amount as NUMBER, but this call does not provide it.", + ], + "tests/typecheck_function_missing_unchecked_arg_bad.claro": [ + "tests/typecheck_function_missing_unchecked_arg_bad.claro:5: Function greet needs 1 argument, but this call gives 0. Add the missing argument.", + ], + "tests/typecheck_function_call_missing_unchecked_arg_bad.claro": [ + "tests/typecheck_function_call_missing_unchecked_arg_bad.claro:5: Function greet needs 1 argument, but this call gives 0. Add the missing argument.", + ], + "tests/typecheck_function_extra_arg_bad.claro": [ + "tests/typecheck_function_extra_arg_bad.claro:6: Function square only accepts 1 argument, but this call gives 2. Remove the extra argument.", + ], + "tests/typecheck_function_unknown_bad.claro": [ + "tests/typecheck_function_unknown_bad.claro:6: Function squre is not known yet. Check the function name or add TEACH squre before calling it.", + ], + "tests/typecheck_function_call_unknown_bad.claro": [ + "tests/typecheck_function_call_unknown_bad.claro:6: Function squre is not known yet. Check the function name or add TEACH squre before calling it.", + ], "tests/typecheck_method_bad.claro": [ "tests/typecheck_method_bad.claro:11: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", ], + "tests/typecheck_method_call_bad.claro": [ + "tests/typecheck_method_call_bad.claro:11: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", + ], + "tests/typecheck_method_second_method_bad.claro": [ + "tests/typecheck_method_second_method_bad.claro:15: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", + ], + "tests/typecheck_method_second_method_extra_arg_bad.claro": [ + "tests/typecheck_method_second_method_extra_arg_bad.claro:15: Method Player.add only accepts 1 argument, but this call gives 2. Remove the extra argument.", + ], + "tests/typecheck_method_missing_arg_bad.claro": [ + "tests/typecheck_method_missing_arg_bad.claro:11: Method Player.add needs argument points as NUMBER, but this call does not provide it.", + ], + "tests/typecheck_method_missing_unchecked_arg_bad.claro": [ + "tests/typecheck_method_missing_unchecked_arg_bad.claro:10: Method Player.rename needs 1 argument, but this call gives 0. Add the missing argument.", + ], + "tests/typecheck_method_call_missing_unchecked_arg_bad.claro": [ + "tests/typecheck_method_call_missing_unchecked_arg_bad.claro:10: Method Player.rename needs 1 argument, but this call gives 0. Add the missing argument.", + ], + "tests/typecheck_method_extra_arg_bad.claro": [ + "tests/typecheck_method_extra_arg_bad.claro:11: Method Player.add only accepts 1 argument, but this call gives 2. Remove the extra argument.", + ], "tests/typecheck_method_unknown_object_bad.claro": [ "tests/typecheck_method_unknown_object_bad.claro:10: Object player is not known yet. Create it with NEW ClassName player before calling player.add.", ], + "tests/typecheck_method_call_unknown_object_bad.claro": [ + "tests/typecheck_method_call_unknown_object_bad.claro:10: Object player is not known yet. Create it with NEW ClassName player before calling player.add.", + ], + "tests/typecheck_method_call_unknown_object_unknown_method_bad.claro": [ + "tests/typecheck_method_call_unknown_object_unknown_method_bad.claro:10: Object player is not known yet. Create it with NEW ClassName player before calling player.fly.", + ], "tests/typecheck_method_unknown_method_bad.claro": [ "tests/typecheck_method_unknown_method_bad.claro:11: Object Player has no method fly. Check the method name or add TEACH fly inside CLASS Player.", ], + "tests/typecheck_method_call_unknown_method_bad.claro": [ + "tests/typecheck_method_call_unknown_method_bad.claro:11: Object Player has no method fly. Check the method name or add TEACH fly inside CLASS Player.", + ], "tests/typecheck_object_field_bad.claro": [ "tests/typecheck_object_field_bad.claro:6: Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT.", ], + "tests/typecheck_object_field_typed_bad.claro": [ + "tests/typecheck_object_field_typed_bad.claro:6: Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_object_field_after_method_bad.claro": [ + "tests/typecheck_object_field_after_method_bad.claro:10: Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT.", + ], "tests/typecheck_object_field_text_bad.claro": [ "tests/typecheck_object_field_text_bad.claro:6: Type mismatch for field player.name: expected TEXT, but this value looks like NUMBER.", ], @@ -44,6 +263,9 @@ EXPECTED = { "tests/typecheck_object_field_unknown_bad.claro": [ "tests/typecheck_object_field_unknown_bad.claro:6: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", ], + "tests/typecheck_object_field_unknown_typed_bad.claro": [ + "tests/typecheck_object_field_unknown_typed_bad.claro:6: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], "tests/typecheck_object_field_unknown_text_bad.claro": [ "tests/typecheck_object_field_unknown_text_bad.claro:6: Object Player has no field nickname. Check the field name or add HAS nickname TEXT to the class.", ], @@ -53,6 +275,27 @@ EXPECTED = { "tests/typecheck_object_field_unknown_expression_bad.claro": [ "tests/typecheck_object_field_unknown_expression_bad.claro:7: Object Player has no field level. Check the field name or add the field to the class with the right type.", ], + "tests/typecheck_object_field_expression_bad.claro": [ + "tests/typecheck_object_field_expression_bad.claro:8: Type mismatch for field player.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_object_field_compound_bad.claro": [ + "tests/typecheck_object_field_compound_bad.claro:8: Type mismatch for field player.score: addition needs NUMBER values, but player.name looks like TEXT.", + ], + "tests/typecheck_object_field_addition_bad.claro": [ + "tests/typecheck_object_field_addition_bad.claro:8: Type mismatch for field player.score: addition needs NUMBER values, but player.name looks like TEXT.", + ], + "tests/typecheck_object_field_subtraction_bad.claro": [ + "tests/typecheck_object_field_subtraction_bad.claro:8: Type mismatch for field player.score: subtraction needs NUMBER values, but player.name looks like TEXT.", + ], + "tests/typecheck_object_field_multiplication_bad.claro": [ + "tests/typecheck_object_field_multiplication_bad.claro:8: Type mismatch for field player.score: multiplication needs NUMBER values, but player.name looks like TEXT.", + ], + "tests/typecheck_object_field_division_bad.claro": [ + "tests/typecheck_object_field_division_bad.claro:8: Type mismatch for field player.score: division needs NUMBER values, but player.name looks like TEXT.", + ], + "tests/typecheck_object_field_text_expression_bad.claro": [ + "tests/typecheck_object_field_text_expression_bad.claro:8: Type mismatch for field player.name: expected TEXT, but this value looks like NUMBER.", + ], "tests/typecheck_object_field_check_type_text_bad.claro": [ "tests/typecheck_object_field_check_type_text_bad.claro:7: Type check failed: expected NUMBER, but player.name looks like TEXT.", ], @@ -74,21 +317,236 @@ EXPECTED = { "tests/typecheck_object_field_check_type_unknown_object_bad.claro": [ "tests/typecheck_object_field_check_type_unknown_object_bad.claro:5: Object player is not known yet. Create it with NEW ClassName player before checking player.score.", ], + "tests/typecheck_invalid_expected_type_bad.claro": [ + "tests/typecheck_invalid_expected_type_bad.claro:2: CHECK TYPE needs a known type such as NUMBER, TEXT, YESNO, LIST, or MAP, but BANANA is not a Claro type.", + ], + "tests/typecheck_missing_is_bad.claro": [ + "tests/typecheck_missing_is_bad.claro:2: CHECK TYPE needs IS. Try: CHECK TYPE score IS NUMBER.", + ], + "tests/typecheck_missing_expected_type_bad.claro": [ + "tests/typecheck_missing_expected_type_bad.claro:2: CHECK TYPE score needs a type after IS. Try: CHECK TYPE score IS NUMBER.", + ], + "tests/typecheck_missing_expression_bad.claro": [ + "tests/typecheck_missing_expression_bad.claro:1: CHECK TYPE needs an expression, IS, and a type. Try: CHECK TYPE score IS NUMBER.", + ], + "tests/typecheck_missing_expression_with_is_bad.claro": [ + "tests/typecheck_missing_expression_with_is_bad.claro:1: CHECK TYPE needs an expression before IS. Try: CHECK TYPE score IS NUMBER.", + ], + "tests/typecheck_missing_type_of_as_bad.claro": [ + "tests/typecheck_missing_type_of_as_bad.claro:2: TYPE OF needs AS. Try: TYPE OF score AS kind.", + ], + "tests/typecheck_extra_type_of_tokens_bad.claro": [ + "tests/typecheck_extra_type_of_tokens_bad.claro:2: TYPE OF score has extra text after result name kind. Keep only the expression, AS, and one result name.", + ], + "tests/typecheck_extra_teach_tokens_bad.claro": [ + "tests/typecheck_extra_teach_tokens_bad.claro:1: Function greet has extra text after return type NUMBER. Keep only the function name, parameters, and one return type.", + ], + "tests/typecheck_missing_return_type_bad.claro": [ + "tests/typecheck_missing_return_type_bad.claro:1: Function greet needs a return type after RETURNS. Add a type such as NUMBER.", + ], + "tests/typecheck_function_compat_missing_return_type_bad.claro": [ + "tests/typecheck_function_compat_missing_return_type_bad.claro:1: Function greet needs a return type after RETURNS. Add a type such as NUMBER.", + ], + "tests/typecheck_method_missing_return_type_bad.claro": [ + "tests/typecheck_method_missing_return_type_bad.claro:2: Method Player.score needs a return type after RETURNS. Add a type such as NUMBER.", + ], + "tests/typecheck_method_compat_missing_return_type_bad.claro": [ + "tests/typecheck_method_compat_missing_return_type_bad.claro:2: Method Player.score needs a return type after RETURNS. Add a type such as NUMBER.", + ], + "tests/typecheck_extra_check_type_tokens_bad.claro": [ + "tests/typecheck_extra_check_type_tokens_bad.claro:2: CHECK TYPE score has extra text after type NUMBER. Keep only the expression, IS, and one type.", + ], + "tests/typecheck_invalid_return_type_bad.claro": [ + "tests/typecheck_invalid_return_type_bad.claro:1: Function square declares an unknown return type BANANA. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.", + ], + "tests/typecheck_method_invalid_return_type_bad.claro": [ + "tests/typecheck_method_invalid_return_type_bad.claro:2: Method Player.score declares an unknown return type BANANA. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.", + ], + "tests/typecheck_unknown_field_type_bad.claro": [ + "tests/typecheck_unknown_field_type_bad.claro:2: Class Player field score uses an unknown type BANANA. Use a Claro type such as NUMBER, TEXT, YESNO, LIST, or MAP.", + ], + "tests/typecheck_missing_field_type_bad.claro": [ + "tests/typecheck_missing_field_type_bad.claro:2: Class Player field score is missing a type. Add a type such as NUMBER, TEXT, YESNO, LIST, or MAP after the field name.", + ], + "tests/typecheck_extra_class_tokens_bad.claro": [ + "tests/typecheck_extra_class_tokens_bad.claro:1: Class Player has extra text after its name. Keep only the class name after CLASS.", + ], + "tests/typecheck_extra_new_tokens_bad.claro": [ + "tests/typecheck_extra_new_tokens_bad.claro:5: NEW Player has extra text after object name player. Keep only the class and object names.", + ], + "tests/typecheck_missing_object_name_bad.claro": [ + "tests/typecheck_missing_object_name_bad.claro:5: NEW Player needs an object name. Add a name after the class, such as NEW Player player.", + ], + "tests/typecheck_missing_new_class_name_bad.claro": [ + "tests/typecheck_missing_new_class_name_bad.claro:1: NEW needs a class name. Add a class and object name, such as NEW Player player.", + ], + "tests/typecheck_extra_field_tokens_bad.claro": [ + "tests/typecheck_extra_field_tokens_bad.claro:2: Class Player field score has extra text after type NUMBER. Keep only the field name and one type.", + ], "tests/typecheck_object_field_unknown_object_bad.claro": [ "tests/typecheck_object_field_unknown_object_bad.claro:5: Object player is not known yet. Create it with NEW ClassName player before setting player.score.", ], + "tests/typecheck_object_field_alias_bad.claro": [ + "tests/typecheck_object_field_alias_bad.claro:7: Type mismatch for field alias.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_object_field_check_type_alias_bad.claro": [ + "tests/typecheck_object_field_check_type_alias_bad.claro:8: Type check failed: expected TEXT, but alias.score looks like NUMBER.", + ], + "tests/typecheck_object_field_check_type_chained_alias_bad.claro": [ + "tests/typecheck_object_field_check_type_chained_alias_bad.claro:8: Type check failed: expected TEXT, but backup.score looks like NUMBER.", + ], + "tests/typecheck_object_field_chained_alias_bad.claro": [ + "tests/typecheck_object_field_chained_alias_bad.claro:8: Type mismatch for field backup.score: expected NUMBER, but this value looks like TEXT.", + ], + "tests/typecheck_object_alias_method_bad.claro": [ + "tests/typecheck_object_alias_method_bad.claro:12: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", + ], + "tests/typecheck_object_alias_method_call_bad.claro": [ + "tests/typecheck_object_alias_method_call_bad.claro:12: Type mismatch for method Player.add: parameter points needs NUMBER, but this argument looks like TEXT.", + ], + "tests/typecheck_object_alias_method_call_extra_arg_bad.claro": [ + "tests/typecheck_object_alias_method_call_extra_arg_bad.claro:12: Method Player.add only accepts 1 argument, but this call gives 2. Remove the extra argument.", + ], + "tests/typecheck_method_field_assignment_bad.claro": [ + "tests/typecheck_method_field_assignment_bad.claro:5: Type mismatch for field score in Player.add: addition needs NUMBER values, but name looks like TEXT.", + ], + "tests/typecheck_method_unknown_field_bad.claro": [ + "tests/typecheck_method_unknown_field_bad.claro:5: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_compat_unknown_field_bad.claro": [ + "tests/typecheck_method_compat_unknown_field_bad.claro:5: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_compat_unknown_field_expression_bad.claro": [ + "tests/typecheck_method_compat_unknown_field_expression_bad.claro:5: Object Player has no field level. Check the field name or add the field to the class with the right type.", + ], + "tests/typecheck_method_unknown_field_typed_bad.claro": [ + "tests/typecheck_method_unknown_field_typed_bad.claro:4: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_compat_unknown_field_typed_bad.claro": [ + "tests/typecheck_method_compat_unknown_field_typed_bad.claro:4: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_typed_field_assignment_bad.claro": [ + "tests/typecheck_method_typed_field_assignment_bad.claro:5: Type mismatch for field score in Player.set_score: class declares NUMBER, but this assignment says TEXT. Use NUMBER for score.", + ], + "tests/typecheck_method_inline_field_annotation_bad.claro": [ + "tests/typecheck_method_inline_field_annotation_bad.claro:5: Type mismatch for field score in Player.set_score: class declares NUMBER, but this assignment says TEXT. Use NUMBER for score.", + ], + "tests/typecheck_method_compat_inline_field_annotation_bad.claro": [ + "tests/typecheck_method_compat_inline_field_annotation_bad.claro:5: Type mismatch for field score in Player.set_score: class declares NUMBER, but this assignment says TEXT. Use NUMBER for score.", + ], + "tests/typecheck_method_inline_field_annotation_unknown_type_bad.claro": [ + "tests/typecheck_method_inline_field_annotation_unknown_type_bad.claro:5: Field score in Player.set_score needs a known type such as NUMBER, TEXT, or YESNO, but BANANA is not a Claro type. Use NUMBER for score.", + ], + "tests/typecheck_method_compat_inline_field_annotation_unknown_type_bad.claro": [ + "tests/typecheck_method_compat_inline_field_annotation_unknown_type_bad.claro:5: Field score in Player.set_score needs a known type such as NUMBER, TEXT, or YESNO, but BANANA is not a Claro type. Use NUMBER for score.", + ], + "tests/typecheck_method_unknown_field_expression_bad.claro": [ + "tests/typecheck_method_unknown_field_expression_bad.claro:5: Object Player has no field level. Check the field name or add the field to the class with the right type.", + ], + "tests/typecheck_method_unknown_field_check_type_bad.claro": [ + "tests/typecheck_method_unknown_field_check_type_bad.claro:5: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_compat_unknown_field_check_type_bad.claro": [ + "tests/typecheck_method_compat_unknown_field_check_type_bad.claro:5: Object Player has no field level. Check the field name or add HAS level NUMBER to the class.", + ], + "tests/typecheck_method_text_field_assignment_bad.claro": [ + "tests/typecheck_method_text_field_assignment_bad.claro:4: Type mismatch for field name in Player.rename: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_method_compat_text_field_assignment_bad.claro": [ + "tests/typecheck_method_compat_text_field_assignment_bad.claro:4: Type mismatch for field name in Player.rename: expected TEXT, but this value looks like NUMBER.", + ], + "tests/typecheck_method_yesno_field_assignment_bad.claro": [ + "tests/typecheck_method_yesno_field_assignment_bad.claro:4: Type mismatch for field ready in Player.toggle: expected YESNO, but this value looks like TEXT.", + ], + "tests/typecheck_method_compat_yesno_field_assignment_bad.claro": [ + "tests/typecheck_method_compat_yesno_field_assignment_bad.claro:4: Type mismatch for field ready in Player.toggle: expected YESNO, but this value looks like TEXT.", + ], + "tests/typecheck_method_field_check_type_bad.claro": [ + "tests/typecheck_method_field_check_type_bad.claro:4: Type check failed in Player.rename: expected NUMBER, but name looks like TEXT.", + ], + "tests/typecheck_method_compat_field_check_type_bad.claro": [ + "tests/typecheck_method_compat_field_check_type_bad.claro:4: Type check failed in Player.rename: expected NUMBER, but name looks like TEXT.", + ], + "tests/typecheck_method_compat_yesno_field_check_type_bad.claro": [ + "tests/typecheck_method_compat_yesno_field_check_type_bad.claro:4: Type check failed in Player.toggle: expected TEXT, but ready looks like YESNO.", + ], } EXPECTED_OK = [ "tests/typecheck_function_good.claro", + "tests/typecheck_function_return_good.claro", + "tests/typecheck_function_compat_return_good.claro", + "tests/typecheck_function_compat_return_addition_good.claro", + "tests/typecheck_function_compat_return_subtraction_good.claro", + "tests/typecheck_function_compat_return_division_good.claro", + "tests/typecheck_function_compat_return_multiplication_good.claro", + "tests/typecheck_function_compat_lowercase_return_good.claro", + "tests/typecheck_function_lowercase_return_good.claro", + "tests/typecheck_function_return_expression_good.claro", + "tests/typecheck_function_return_multiplication_good.claro", + "tests/typecheck_function_return_division_good.claro", + "tests/typecheck_function_return_subtraction_good.claro", + "tests/typecheck_function_unknown_return_expression_good.claro", + "tests/typecheck_function_branch_complete_good.claro", + "tests/typecheck_function_nested_branch_complete_good.claro", + "tests/typecheck_function_compat_branch_complete_good.claro", + "tests/typecheck_method_return_good.claro", + "tests/typecheck_method_field_return_good.claro", + "tests/typecheck_method_compat_field_return_good.claro", + "tests/typecheck_method_compat_return_good.claro", + "tests/typecheck_method_compat_lowercase_return_good.claro", + "tests/typecheck_method_compat_return_division_good.claro", + "tests/typecheck_method_compat_return_subtraction_good.claro", + "tests/typecheck_method_compat_return_multiplication_good.claro", + "tests/typecheck_method_compat_return_addition_good.claro", + "tests/typecheck_method_return_expression_good.claro", + "tests/typecheck_method_return_multiplication_good.claro", + "tests/typecheck_method_return_division_good.claro", + "tests/typecheck_method_return_subtraction_good.claro", + "tests/typecheck_method_return_addition_good.claro", + "tests/typecheck_method_unknown_return_expression_good.claro", + "tests/typecheck_method_field_assignment_good.claro", + "tests/typecheck_method_text_field_assignment_good.claro", + "tests/typecheck_method_yesno_field_assignment_good.claro", + "tests/typecheck_method_compat_text_field_assignment_good.claro", + "tests/typecheck_method_compat_yesno_field_assignment_good.claro", + "tests/typecheck_method_compat_field_assignment_good.claro", + "tests/typecheck_method_text_concat_good.claro", + "tests/typecheck_method_compat_text_concat_good.claro", + "tests/typecheck_method_field_check_type_good.claro", + "tests/typecheck_method_compat_field_check_type_good.claro", + "tests/typecheck_method_compat_yesno_field_check_type_good.claro", + "tests/typecheck_method_yesno_field_check_type_good.claro", + "tests/typecheck_method_inline_field_annotation_good.claro", + "tests/typecheck_method_compat_inline_field_annotation_good.claro", + "tests/typecheck_method_nested_branch_complete_good.claro", "tests/typecheck_function_multi_good.claro", "tests/typecheck_method_good.claro", + "tests/typecheck_method_call_good.claro", "tests/typecheck_object_field_good.claro", + "tests/typecheck_object_field_expression_good.claro", + "tests/typecheck_object_field_compound_good.claro", + "tests/typecheck_object_field_subtraction_good.claro", + "tests/typecheck_object_field_division_good.claro", + "tests/typecheck_object_field_multiplication_good.claro", "tests/typecheck_object_field_text_good.claro", + "tests/typecheck_object_field_text_concat_good.claro", + "tests/typecheck_object_field_text_concat_reverse_good.claro", + "tests/typecheck_object_field_text_concat_fields_good.claro", "tests/typecheck_object_field_yesno_good.claro", "tests/typecheck_object_field_check_type_number_good.claro", "tests/typecheck_object_field_check_type_text_good.claro", + "tests/typecheck_object_field_check_type_yesno_good.claro", "tests/typecheck_object_field_check_type_text_yesno_good.claro", + "tests/typecheck_same_field_name_different_classes_good.claro", + "tests/typecheck_object_field_alias_good.claro", + "tests/typecheck_object_field_check_type_chained_alias_good.claro", + "tests/typecheck_object_field_check_type_chained_alias_text_good.claro", + "tests/typecheck_object_alias_method_call_good.claro", + "tests/typecheck_object_alias_method_do_good.claro", + "tests/typecheck_nested_container_good.claro", + "tests/typecheck_good.claro", + "tests/typecheck_container_good.claro", "tests/37_object_field_types.claro", ] @@ -101,6 +559,23 @@ STALE_PHRASES = [ ] +def validate_fixture_coverage(): + discovered = { + path.relative_to(ROOT).as_posix() + for path in (ROOT / "tests").glob("typecheck_*.claro") + } + listed = set(EXPECTED) | set(EXPECTED_OK) + missing = sorted(discovered - listed) + extra = sorted(listed - discovered - {"tests/37_object_field_types.claro"}) + if missing or extra: + details = [] + if missing: + details.append("unlisted fixtures: " + ", ".join(missing)) + if extra: + details.append("missing fixture files: " + ", ".join(extra)) + raise SystemExit("Typecheck fixture coverage mismatch: " + "; ".join(details)) + + def run_typecheck(path): result = subprocess.run( [str(EXE), "typecheck", path], @@ -147,6 +622,7 @@ def main(): if not EXE.exists(): raise SystemExit(f"Missing Claro executable: {EXE}") + validate_fixture_coverage() for path in EXPECTED: run_typecheck(path)