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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- NTNT coding guide sections are sourced from docs/AI_AGENT_GUIDE.md -->
<!-- To update NTNT coding instructions, edit AI_AGENT_GUIDE.md and copy to all agent files -->
<!-- Last synced: 2026-03-11 -->
<!-- Last synced: 2026-03-12 -->

# NTNT Language - GitHub Copilot Instructions

Expand Down Expand Up @@ -577,7 +577,7 @@ let x = if true { 1 } // ERROR: If-expressions require an else branch

### 11. Destructuring Assignment

Map, array, and nested destructuring in `let` bindings, `match`, and `for` loops:
Map, array, and nested destructuring in `let` bindings, `match`, `for` loops, and function parameters:

```ntnt
// Map destructuring
Expand Down Expand Up @@ -615,6 +615,20 @@ match data {
{ name, age } => print("{name} is {age}"),
_ => print("no match")
}

// Function parameter destructuring
fn greet({ name, email }) {
print("Hello {name} ({email})")
}

fn first_two([a, b, ...rest]) {
print("{a}, {b}")
}

// With type annotation
fn process({ name, email }: Map) {
print("{name}: {email}")
}
```

### 12. Regex Capture Groups
Expand Down Expand Up @@ -1050,6 +1064,7 @@ The type checker tracks types through common operations:
- **`parse_csv()`** — returns `Array<Array<String>>`
- **Match arm narrowing** — `Ok(data)` on `Result<T, E>` binds `data` as `T`; `Some(x)` on `Option<T>` binds `x` as `T`; struct patterns bind field types
- **Cross-file imports** — `import { foo } from "./lib/utils"` resolves function signatures from the imported `.tnt` file
- **Circular import detection** — if files form an import cycle (e.g. `a.tnt → b.tnt → a.tnt`), a warning is emitted showing the exact chain

### What the Type Checker Catches

Expand Down
19 changes: 17 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<!-- NTNT coding guide sections are sourced from docs/AI_AGENT_GUIDE.md -->
<!-- To update NTNT coding instructions, edit AI_AGENT_GUIDE.md and copy to all agent files -->
<!-- Last synced: 2026-03-11 -->
<!-- Last synced: 2026-03-12 -->

# NTNT Language - Claude Code Instructions

Expand Down Expand Up @@ -597,7 +597,7 @@ let x = if true { 1 } // ERROR: If-expressions require an else branch

### 11. Destructuring Assignment

Map, array, and nested destructuring in `let` bindings, `match`, and `for` loops:
Map, array, and nested destructuring in `let` bindings, `match`, `for` loops, and function parameters:

```ntnt
// Map destructuring
Expand Down Expand Up @@ -635,6 +635,20 @@ match data {
{ name, age } => print("{name} is {age}"),
_ => print("no match")
}

// Function parameter destructuring
fn greet({ name, email }) {
print("Hello {name} ({email})")
}

fn first_two([a, b, ...rest]) {
print("{a}, {b}")
}

// With type annotation
fn process({ name, email }: Map) {
print("{name}: {email}")
}
```

### 12. Regex Capture Groups
Expand Down Expand Up @@ -1070,6 +1084,7 @@ The type checker tracks types through common operations:
- **`parse_csv()`** — returns `Array<Array<String>>`
- **Match arm narrowing** — `Ok(data)` on `Result<T, E>` binds `data` as `T`; `Some(x)` on `Option<T>` binds `x` as `T`; struct patterns bind field types
- **Cross-file imports** — `import { foo } from "./lib/utils"` resolves function signatures from the imported `.tnt` file
- **Circular import detection** — if files form an import cycle (e.g. `a.tnt → b.tnt → a.tnt`), a warning is emitted showing the exact chain

### What the Type Checker Catches

Expand Down
7 changes: 4 additions & 3 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ fn divide(a: Int, b: Int) -> Int
- [x] Union type soundness: union VALUES require ALL members compatible with target, union TARGETS require ANY member match
- [x] Union type flattening and deduplication in `union_type()` computation
- [x] Block divergence analysis (`block_diverges`) for otherwise/lambda validation
- [x] `otherwise` block divergence warning (must end with return/break/continue)
- [x] `otherwise` block divergence enforcement (lint error, not just warning — must end with return/break/continue)
- [x] Lambda return type inference via `collected_returns` (early returns included in union)
- [x] Flow-sensitive type narrowing after guards (`if x == None { return }` narrows `x` to inner type)
- [x] Narrowing patterns: `x == None`, `x != None`, `is_some(x)`, `is_none(x)`, `is_ok(x)`, `is_err(x)`, `!cond`
Expand All @@ -531,6 +531,7 @@ fn divide(a: Int, b: Int) -> Int
- [x] Variadic functions check minimum argument count
- [x] Cross-file Pass 2 inference: unannotated exported functions get inferred return types
- [x] Circular import safety: shared module cache prevents infinite recursion during cross-file analysis
- [x] Circular import diagnostics: cycle chain shown in warnings (e.g. `a.tnt → b.tnt → a.tnt`)
- [x] Strict mode: string interpolation warning for complex types (Array, Map, Function)
- [x] Strict mode: Float→Int precision loss warning
- [ ] Cross-file struct/enum propagation (extend import type resolution to include struct and enum definitions)
Expand Down Expand Up @@ -639,7 +640,7 @@ for line in lines {
- [x] Works with `Option<T>` — unwrap Some or run otherwise on None
- [x] Verify otherwise block diverges (return/break/continue)
- [x] Type checker unwraps `Result<T, E>` → `T` and `Option<T>` → `T` for let with otherwise
- [ ] Lint integration: warn if otherwise block doesn't diverge
- [x] Lint integration: error if otherwise block doesn't diverge

### 7.3 Anonymous Functions / Closures ✅

Expand Down Expand Up @@ -865,7 +866,7 @@ fn create_user({ name, email }: Map) -> User {
- [x] Rest patterns: `let [head, ...tail] = arr`, `let { name, ...other } = map`
- [x] Nested destructuring: `let { user: { name } } = data`
- [ ] Destructuring with type annotations
- [ ] Destructuring in function parameters
- [x] Destructuring in function parameters
- [x] Destructuring in `for` loops: `for { name, email } in users { ... }`
- [x] Type checking: destructured fields are type-inferred from the source expression
- [x] Map destructuring with rename: `let { name: n } = data`
Expand Down
17 changes: 16 additions & 1 deletion docs/AI_AGENT_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ let x = if true { 1 } // ERROR: If-expressions require an else branch

### 11. Destructuring Assignment

Map, array, and nested destructuring in `let` bindings, `match`, and `for` loops:
Map, array, and nested destructuring in `let` bindings, `match`, `for` loops, and function parameters:

```ntnt
// Map destructuring
Expand Down Expand Up @@ -608,6 +608,20 @@ match data {
{ name, age } => print("{name} is {age}"),
_ => print("no match")
}

// Function parameter destructuring
fn greet({ name, email }) {
print("Hello {name} ({email})")
}

fn first_two([a, b, ...rest]) {
print("{a}, {b}")
}

// With type annotation
fn process({ name, email }: Map) {
print("{name}: {email}")
}
```

### 12. Regex Capture Groups
Expand Down Expand Up @@ -1043,6 +1057,7 @@ The type checker tracks types through common operations:
- **`parse_csv()`** — returns `Array<Array<String>>`
- **Match arm narrowing** — `Ok(data)` on `Result<T, E>` binds `data` as `T`; `Some(x)` on `Option<T>` binds `x` as `T`; struct patterns bind field types
- **Cross-file imports** — `import { foo } from "./lib/utils"` resolves function signatures from the imported `.tnt` file
- **Circular import detection** — if files form an import cycle (e.g. `a.tnt → b.tnt → a.tnt`), a warning is emitted showing the exact chain

### What the Type Checker Catches

Expand Down
2 changes: 2 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,8 @@ pub struct Parameter {
pub name: String,
pub type_annotation: Option<TypeExpr>,
pub default: Option<Expression>,
#[serde(default)]
pub pattern: Option<Pattern>,
}

/// Import item for selective imports: `import { foo as bar } from "module"`
Expand Down
72 changes: 71 additions & 1 deletion src/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6339,7 +6339,12 @@ impl Interpreter {
// Should not reach here due to arity check above
Value::Unit
};
func_env.borrow_mut().define(param.name.clone(), value);
if let Some(ref pat) = param.pattern {
// Destructured param: only bind pattern variables, not the synthetic name
self.bind_pattern(pat, &value)?;
} else {
func_env.borrow_mut().define(param.name.clone(), value);
}
}

// Environment is already set to func_env for contract checking and body execution
Expand Down Expand Up @@ -12017,4 +12022,69 @@ page
other => panic!("expected String('ab'), got {:?}", other),
}
}

#[test]
fn test_destructured_map_param() {
let result = eval(
r#"
fn greet({ name, email }) {
return name + " <" + email + ">"
}
greet(map { "name": "Alice", "email": "a@b.com" })
"#,
)
.unwrap();
match result {
Value::String(s) => assert_eq!(s, "Alice <a@b.com>"),
other => panic!("expected String, got {:?}", other),
}
}

#[test]
fn test_destructured_map_param_with_type() {
let result = eval(
r#"
fn greet({ name, email }: Map) -> String {
return name + " <" + email + ">"
}
greet(map { "name": "Bob", "email": "b@b.com" })
"#,
)
.unwrap();
match result {
Value::String(s) => assert_eq!(s, "Bob <b@b.com>"),
other => panic!("expected String, got {:?}", other),
}
}

#[test]
fn test_destructured_array_param() {
let result = eval(
r#"
fn first_two([a, b, ...rest]) {
return a + b
}
first_two([10, 20, 30])
"#,
)
.unwrap();
assert!(matches!(result, Value::Int(30)));
}

#[test]
fn test_destructured_param_with_regular_params() {
let result = eval(
r#"
fn process(id, { name }) {
return str(id) + ": " + name
}
process(42, map { "name": "Alice" })
"#,
)
.unwrap();
match result {
Value::String(s) => assert_eq!(s, "42: Alice"),
other => panic!("expected String, got {:?}", other),
}
}
}
12 changes: 11 additions & 1 deletion src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,15 @@ impl Parser {

if !self.check(&TokenKind::RightParen) {
loop {
let name = self.consume_identifier("Expected parameter name")?;
let (name, pattern) =
if self.check(&TokenKind::LeftBrace) || self.check(&TokenKind::LeftBracket) {
let pat = self.parse_pattern()?;
let synth_name = format!("_destructure_{}", params.len());
(synth_name, Some(pat))
} else {
let name = self.consume_identifier("Expected parameter name")?;
(name, None)
Comment on lines +311 to +317
Copy link

Copilot AI Mar 11, 2026

Choose a reason for hiding this comment

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

The synthetic parameter name (_destructure_{n}) is used as Parameter.name for destructured parameters. This can leak into user-facing diagnostics (e.g., the “required parameter cannot follow a parameter with a default” error will report _destructure_1, which doesn't exist in source code). Consider storing a separate display name (derived from the pattern) for diagnostics, or enhancing error formatting to refer to the pattern/first bound identifier instead of the synthetic name.

Copilot uses AI. Check for mistakes.
};

let type_annotation = if self.match_token(&[TokenKind::Colon]) {
Some(self.parse_type()?)
Expand All @@ -325,6 +333,7 @@ impl Parser {
name,
type_annotation,
default,
pattern,
});

if !self.match_token(&[TokenKind::Comma]) {
Expand Down Expand Up @@ -522,6 +531,7 @@ impl Parser {
name: param_name,
type_annotation,
default: None,
pattern: None,
});
}

Expand Down
Loading
Loading