v0.4.0: Type Safety Modes — Two Axes of Type Control (DD-009)#19
v0.4.0: Type Safety Modes — Two Axes of Type Control (DD-009)#19joshcramer merged 11 commits intomainfrom
Conversation
v0.4.0: Introduces two independent axes for type control: Axis 1 — Runtime (NTNT_TYPE_MODE env var): - strict: type mismatches are runtime errors (fail-closed) - warn: type mismatches log [WARN] and continue (default) - forgiving: silent degradation (pre-v0.4 behavior) Axis 2 — Lint (NTNT_LINT_MODE env var): - default: only check annotated code - warn: also warn on missing annotations - strict: missing annotations are errors Modifies all DD-008 resilience points (index type mismatch, for..in on non-collections, template error boundaries) to respect TypeMode. Includes 5 new runtime tests with mutex-based env var isolation for parallel test safety. All 945 tests pass. Also includes intent check TCP connect fix (replaces HTTP poll with TCP connect + subprocess death detection). Design doc: DD-009 (plans/009-ntnt-type-safety-modes.md)
There was a problem hiding this comment.
Pull request overview
This PR introduces DD-009’s “two-axis” type control by adding runtime TypeMode and lint-time LintMode configuration, then wiring TypeMode into previously “resilient” runtime behaviors (indexing, for..in, and template error boundaries). It also updates intent-check server readiness probing and bumps the crate version to v0.4.0.
Changes:
- Add
src/config.rswithTypeMode/LintModeenums and env-driven getters (cached outside tests). - Update interpreter resilience points to follow
NTNT_TYPE_MODEsemantics (strict/warn/forgiving), plus add tests covering the new modes. - Change intent-check readiness from HTTP polling to TCP connect probing; bump version to 0.4.0.
Reviewed changes
Copilot reviewed 5 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main.rs | Switch intent-check server readiness probe to TCP connect + early-exit detection. |
| src/lib.rs | Export new config module from the crate. |
| src/interpreter.rs | Apply TypeMode to for..in, indexing mismatches, and template error boundaries; add TypeMode-related tests. |
| src/config.rs | New env-based configuration module for runtime/lint type modes with test-aware caching behavior. |
| Cargo.toml | Bump package version to 0.4.0. |
| Cargo.lock | Update locked package version to 0.4.0. |
Copilot review fixes: - Fix config.rs doc wording (production → non-test builds) - Gate template error HTML comments behind is_production_mode() in warn mode to prevent leaking internal details to end users Type system extensions (DD-009 Phase 2+3): - Add check_program_with_lint_mode() using LintMode enum - Strict lint mode now promotes annotation warnings to errors - Add --warn-untyped CLI flag for non-fatal annotation warnings - NTNT_LINT_MODE env var wired into lint command (with precedence) - Lambda parameters get annotation warnings in warn/strict mode - Field access on non-struct/map is now TypeMode-aware CI fixes: - Reduce recursion limit test depth (3 vs 10) for macOS stack safety - Guard test_for_in_string_skips against parallel env var races - Regenerate docs (STDLIB, SYNTAX, IAL, RUNTIME references) - Update integration tests: strict mode annotation issues are errors All 945 tests pass (parallel execution).
Copilot Review ResponsesTCP readiness probe (src/main.rs): Valid point about false positives. For intent checks this is acceptable — port is randomly assigned via config.rs doc wording: ✅ Fixed in 0ab1460 — changed to "non-test builds" to match Template error HTML comments leaking (both instances): ✅ Fixed in 0ab1460 — added Additional changes in this push:
|
- NTNT_LINT_MODE=default now properly overrides NTNT_STRICT (check if env var is explicitly set before falling back to legacy config) - Field access strict mode uses RuntimeError (consistent with index) - test_for_in_warn_skips asserts count==0 (verifies loop body skipped) - Strict lint integration tests assert non-zero exit code - All 945 tests pass
Second Review Round — All 5 Comments Addressed (24a1c18)
|
6.1 Documentation:
- NTNT_TYPE_MODE and NTNT_LINT_MODE in runtime.toml (auto-generates to docs)
- Type Safety Modes section with both axes, examples, Docker config
- --warn-untyped flag documented in lint command
- CLAUDE.md and copilot-instructions.md updated via AI_AGENT_GUIDE.md
6.2 Template error consolidation:
- New handle_template_error() replaces 6 copy-pasted match blocks
- Single function handles strict/warn/forgiving for all template boundaries
6.3 Warning deduplication:
- Thread-local HashSet tracks already-warned locations
- type_warn_dedup() prevents spam from loops over bad data
- clear_type_warnings() for per-request reset
6.4 Error path categorization:
- Added comprehensive doc comment categorizing all ~130 error paths
- Categories: TypeMode-aware, code bugs, explicit conversions,
arithmetic invariants, control flow/internal
6.5 NTNT_STRICT deprecation:
- std::sync::Once-guarded deprecation warning on stderr
- Directs users to NTNT_LINT_MODE=strict
- Still works (backward compatible)
6.6 --warn-untyped integration tests:
- test_warn_untyped_produces_warnings_not_errors
- test_warn_untyped_no_warnings_on_fully_typed
- Verifies exit code 0 (non-fatal warnings)
All 947 tests pass.
- Fix handle_template_error doc comment (said bool, returns Result) - Wire up clear_type_warnings() at eval() and call_function_by_name() boundaries to prevent unbounded growth and suppressed warnings - Make strict_check_with_file() respect NTNT_LINT_MODE=strict (not just deprecated NTNT_STRICT) - Make --strict and --warn-untyped mutually exclusive (conflicts_with) - Fix --warn-untyped description to cover all strict-mode warnings - Fix variable shadowing: code -> source/exit_code in 3 tests - Add DiagnosticKind enum for structured diagnostic classification - Replace brittle substring matching in check_program_with_lint_mode with DiagnosticKind matching (MissingParamAnnotation, etc.) All 947 tests pass.
|
Addressed all 10 new Copilot comments in Structural improvements:
CLI/docs:
Code quality:
947 tests passing. |
7.1 Error message quality:
- TypeContext struct with expected/got/hint for rich errors
- IntentError::type_error/runtime_error constructors (backward compat)
- type_error_with_context/runtime_error_with_context for rich errors
- rich_display() with │ expected: / │ found: / └─ hint: formatting
- format_error() in main.rs now shows expected/got/hint sections
- for..in, index mismatch, field access errors enriched with context
- Binary op catch-all now shows operator symbol + String+concat hint
- len() error enriched with type context
- Bulk migration: 747 IntentError::TypeError/RuntimeError call sites
converted to constructor pattern
7.2 T? shorthand + type aliases:
- Function type annotations: (T1, T2) -> ReturnType in type positions
- [T] array type literal syntax in annotations
- type UserId = Int, type Handler = (Int) -> String all working
- Parser extended: parse_single_type handles (, [ prefixes
7.3 Deeper type inference (already largely working):
- Return type inference through function chains: verified working
- Collection literal inference: [1,2,3] → [Int]: verified working
- Lambda param inference from context: verified working via tests
7.4 Full generics in type checker:
- FunctionSig.type_params stores generic param names
- Type params preserved as Type::Named (not collapsed to Any)
- unify_type_params(): infers T from argument types
- unify_one(): recursive structural unification
- substitute_type_params(): replaces T with concrete type in return
- Generic param args skip arity type checking (accept any type)
- identity<T>(42) correctly infers return type Int
- Wrong generic usage caught: identity<T>(42) assigned to String
Tests: 956 passing (added 5 language_features_tests + 4 error.rs tests)
|
Phase 7.1–7.4 pushed in 7.1 Error messages (Elm/Rust-tier):
7.2 Type syntax:
7.3 Inference: Already deeper than I thought — return type inference, collection inference, lambda context inference all verified working. 7.4 Real generics:
956 tests passing. |
- Generic unification now detects conflicting type param bindings: fn f<T>(a: T, b: T) called with (Int, String) emits type error 'Type parameter T: conflicting types Int and String' - Fixed let vs let mut in test_array_type_literal_syntax (misleading) - Added 4 generic unification tests: - identity<T> correct usage (no error) - identity<T> wrong assignment (catches mismatch) - merge<T>(Int, String) conflicting T (catches conflict) - first<T>([Int]) array unification (resolves T from [T]) 960 tests passing.
|
Round 4 addressed in
960 tests, all passing. |
- Parser: fix EOF reporting line 0 — current_line/current_column now fall back to previous() token when at EOF - Parser: wrap all declarations (let, fn, struct, enum, import, etc.) with Located so runtime errors propagate line info correctly - Error types: add 'line' field to TypeError, RuntimeError, UndefinedVariable, UndefinedFunction, ArityMismatch - Error display: show file:line header + source snippet with context for all runtime errors (matches existing parser error format) - at_line() builder method: annotate any error with line info at the eval_statement Located boundary Before: 'error[E003] --> Type error: Cannot apply ...' (no location) After: 'error[E003] --> /tmp/foo.tnt:3' + source snippet 960 tests passing
Round 5 — Line Number Accuracy FixProblems found
Fixes
Before vs after960 tests passing. |
| // Skip type-checking for generic type params — they accept any type. | ||
| let is_type_param = | ||
| matches!(param_type, Type::Named(n) if sig.type_params.contains(n)); | ||
| if is_type_param { | ||
| continue; | ||
| } | ||
| if !self.compatible(arg_type, param_type) | ||
| && !matches!(arg_type, Type::Any) | ||
| && !matches!(param_type, Type::Any) |
There was a problem hiding this comment.
Generic argument type-checking currently only skips parameters whose type is exactly a type param (e.g., T). For parameters like [T], Option<T>, Map<String, T>, the pre-unification compatibility check will still fail against concrete typed arguments (e.g., [Int]), because Type::Named("T") is not compatible with Int. Consider unifying first and substituting type params into parameter types before checking, or extending the skip logic to recursively detect type params inside compound types so generic functions can be called with typed collections.
…gs #69, #73) All three module loading paths (load_module_exports, process_route_file, load_middleware_file) now call define_stdlib() alongside define_builtins() and define_builtin_types(). Previously only the main interpreter constructor called define_stdlib(), so lib/route/middleware modules couldn't resolve std/ imports properly. This fixes: - #69: concat() unavailable in lib context - #73: Multiple builtins (get_env, fetch, concat) missing in lib context 960 tests passing
- main.rs: check_stmt_for_issues, import collision check, and collect_used_names now call unwrap_located() before pattern matching. Without this, lint would silently skip all declaration-level statements (let, fn, struct, etc.) since they're now wrapped in Located by the parser. - error.rs: remove unnecessary #[allow(dead_code)] on line fields (fields are used by at_line() and line()) 960 tests passing, zero clippy warnings from changed files.
- README.md: expanded type system description (generics, type aliases, T?, two-axis control), added --warn-untyped to CLI commands - CLAUDE.md, copilot-instructions.md, AI_AGENT_GUIDE.md: added type syntax section (T?, type aliases, array types, generics, error msgs) - syntax.toml: added 5 new type syntax entries (optional_shorthand, type_alias, function_type, array_type, generics) - main.rs: added new type categories to SYNTAX_REFERENCE generator - SYNTAX_REFERENCE.md: regenerated with new type syntax sections 960 tests passing.
v0.4.0: Type Safety Modes — Two Axes of Type Control (DD-009)
Design doc:
plans/009-ntnt-type-safety-modes.md10 commits | 43 files changed | +2,558 / -959 lines | 960 tests passing
What this PR does
Introduces two independent axes of type control, world-class error messages, generics in the type checker, accurate line numbers on all runtime errors, and unified execution contexts for lib/route/middleware modules.
Lint axis (
NTNT_LINT_MODE/--warn-untyped/--strict) — controls how thoroughly code is statically analyzed before it runs.Runtime axis (
NTNT_TYPE_MODE) — controls what happens when types mismatch at runtime:strict→ crash (fail-closed, for auth/payments/safety-critical apps)warn→ log[WARN]+ continue (default)forgiving→ silent degradation (pre-v0.4 behavior)Commits
dd4122cTypeMode/LintModeenums,config.rs, 7 TypeMode-aware resilience points, version → 0.4.00ab1460--warn-untyped/--strictflags, field access TypeMode, test fixes24a1c18NTNT_LINT_MODEprecedence bug,RuntimeErrorconstructor consistency0f1d861handle_template_error()DRY, warning dedup, error categorization,NTNT_STRICTdeprecation, docs22eb976DiagnosticKindenum,clear_type_warnings()at boundaries,--strict/--warn-untypedconflict4e7a092TypeContexterror messages,T?+ type aliases, deeper inference, real genericscc175aeTconsistency), 4 new generic testse5fe366Locatedwrapping for all declarations,linefield on 5 error types, source snippets3d32eacdefine_stdlib()in all 3 module loading paths — fixes findings #69, #737cbab97unwrap_located()in 3 lint paths, remove unnecessary#[allow(dead_code)]Feature breakdown
Two Axes of Type Control (Phases 1–3)
TypeModeenum:Strict/Warn/Forgiving— 7+ TypeMode-aware resilience points:[]) type mismatchfor..inon non-collection valueshandle_template_error())LintModeenum:Default/Warn/StrictNTNT_TYPE_MODE,NTNT_LINT_MODEenv vars —OnceLockcaching in prod, re-read in tests--warn-untyped(warnings, non-fatal) and--strict(errors, fatal), mutually exclusiveNTNT_STRICTdeprecated with[DEPRECATED]message →NTNT_LINT_MODE=strictHashSet— same location warns once per request, not 50×DiagnosticKindenum replaces brittle substring matching for lint promotionWorld-Class Error Messages (Phase 7.1)
TypeContextstruct:expected/got/hintfields│ expected:/└─ hint:formatting and colorTypeError(String)→type_error(msg)constructorsT? Shorthand + Type Aliases (Phase 7.2)
T?shorthand forOptional<T>in annotations[Int],[String](T1, T2) -> ReturnTypetype Handler = (Request) -> Response— function type aliasesDeeper Type Inference (Phase 7.3)
Real Generics in Type Checker (Phase 7.4)
FunctionSig.type_paramsstores generic parameter namesunify_type_params()+unify_one()+substitute_type_params()— structural unificationidentity<T>(42)infers return typeInt(notAny)fn f<T>(a: T, b: T)called with(Int, String)correctly errorsType::Namedinstead of collapsing toAnyAccurate Line Numbers on All Errors (Phase 8.1)
Root cause found:
declaration()in parser calledlet_declaration(),function_declaration(), etc. directly — none were wrapped inLocated. Onlystatement()(if/while/for/return) was wrapped. Every runtime error from alet/fn/struct/importhad zero line info.declaration()now captures line/col before parsing and wraps all results inLocatedTypeError,RuntimeError,UndefinedVariable,UndefinedFunction,ArityMismatchall carry alinefieldat_line()builder method —Locatedhandler annotates errors as they propagateformat_error()showsfile:lineheader + source snippet with context for all error typesUnified Execution Contexts (Phase 8.2)
Root cause:
load_module_exports(),process_route_file(),load_middleware_file()all created a freshEnvironmentand calleddefine_builtins()— but notdefine_stdlib(). The stdlib module registry was empty, soimport { concat } from "std/string"couldn't resolve in lib files.define_stdlib()to all three module loading pathsconcat()unavailable in lib) and #73 (multiple stdlib builtins missing in lib)Self-Review Catch (Phase 8.3)
Bug found during review:
check_stmt_for_issues(), the import collision checker, andcollect_used_names()inmain.rsall pattern-matched directly onStatement::Let,Statement::Function, etc. Sincedeclaration()now wraps these inLocated, they silently fell through to_ => {}— all declaration-level lint checks were being skipped.unwrap_located()calls in the three affected lint pathsTesting
960 tests (up from 919 at branch start, +41 new):
LocatedwrappingStaging validated on v0.4.0 binary:
ntnt lint --warn-untyped: 230 annotation warnings across 47 files ✅ntnt lint --strict: annotations promoted to errors ✅Breaking changes
None. All new behavior is opt-in.
NTNT_TYPE_MODE=warn— logs warnings but does not crash (same behavior as DD-008 forgiving + visibility)NTNT_LINT_MODE=default— only checks annotated code (same as before)NTNT_STRICTstill works but prints a deprecation noticeWhat's not in this PR (future work)
"str" + 42, truthy/falsy in if) — language design decisions needing separate discussionfn sum<T: Numeric>), generic structs