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
3 changes: 2 additions & 1 deletion docs/DIAGNOSTICS.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,9 @@ unallocated rather than implicitly free.
| `H0633` | error | reserved file-read built-in name redeclared | A user task attempts to redeclare the exact `files_read_text` builtin name. |
| `H0634` | error | canonical native program layout | Native canonical admission requires matching `programs/<name>.hum`, `module programs.<name>`, and `app <name>` identity, with one module first, optional local types before the sole final app, and its declared entry task first. |
| `H0635` | error | unsupported native program feature | A layout-valid native request has no supported typed feature (`native_feature_not_supported_v0`) or has an ambiguous typed match (`native_feature_ambiguous_v0`). Owner `native_program` emits it at `native_admission`, after semantic/H0634 blockers and before backend input, JIT, output, or readiness. |
| `H0636` | error | invalid text_split call | A `text_split` call is rejected: it takes exactly two `Text` arguments and its separator must not be a directly-written empty literal. |
| `H0636` | error | invalid text_split call | A `text_split` call is rejected: it takes exactly two `Text` arguments, its separator must not be a directly-written empty literal, and it must not contain a stray empty argument. |
| `H0637` | error | reserved text-split built-in name redeclared | A user task attempts to redeclare the exact `text_split` built-in name. |
| `H0638` | error | invalid text escape | A text literal contains an unknown escape sequence or a trailing backslash. Only `\n`, `\t`, `\\`, and `\"` are accepted (decision 0022). |

Note: `TextSplitError.SepEmpty` is not a checker diagnostic and holds no H-code.
It is a runtime typed failure variant raised when a computed (non-literal)
Expand Down
23 changes: 21 additions & 2 deletions docs/LANGUAGE_REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,25 @@ lines as text for graph facts and future lowering. It is not the final grammar
engine. See [MILESTONE_0_GRAMMAR.md](MILESTONE_0_GRAMMAR.md) for the exact
Milestone 0 parser contract.

### Text Literals and Escapes

Text literals are double-quoted. The canonical AST decodes escape sequences
exactly once, at parse time (decision 0022). Exactly four escapes are
accepted:

| Escape | Decodes to |
|--------|------------|
| `\n` | U+000A (newline) |
| `\t` | U+0009 (tab) |
| `\\` | U+005C (backslash) |
| `\"` | U+0022 (quote) |

Any other escape sequence (e.g. `\q`, `\é`) is a checker error, H0638,
spanning exactly the backslash plus the escaped character. A backslash at the
end of the literal (before the closing quote) is an unterminated-escape error,
also H0638. An escaped quote never terminates the literal: all quote-aware
scanners agree that `\"` does not close the string.

## Top-Level Forms

Current Milestone 0 recognizes these item kinds:
Expand Down Expand Up @@ -698,8 +717,8 @@ Fallibility is conditional on the separator's written form:
- A directly-written non-empty literal separator is infallible and needs no
`try`: `text_split("a,b,c", ",")` type-checks as `List Text` without H0901.
- Every other separator form requires `try`, including a variable bound to a
literal (`let sep = ","; text_split(text, sep)` still raises H0901 without
`try`). No constant-variable flow analysis recovers the literal.
literal (`let sep = ","` then `text_split(text, sep)` still raises H0901
without `try`). No constant-variable flow analysis recovers the literal.
- A directly-written empty separator is a checker error, H0636.
- A runtime-computed empty separator raises the typed failure
`TextSplitError.SepEmpty` through normal `try`/`fail`; it is not an H-code.
Expand Down
14 changes: 14 additions & 0 deletions fixtures/diagnostics/text_invalid_escape_fail.hum
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module fixtures.diagnostics.text_invalid_escape_fail

task invalid_escape() -> Text {
why:
misuse fixture: an unknown escape sequence is a checker error (H0638)

cost:
time: O(1)
space: O(1)
check: warn

does:
return "bad\qescape"
}
16 changes: 16 additions & 0 deletions fixtures/diagnostics/text_split_stray_empty_argument_fail.hum
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
module fixtures.diagnostics.text_split_stray_empty_argument_fail

task split_stray_empty() -> List Text {
why:
misuse fixture: a stray empty argument is a checker error (H0636)

cost:
time: O(1)
space: O(1)
check: warn

does:
let line = "a,b,c"
let pieces = text_split(line,, ",")
return pieces
}
15 changes: 15 additions & 0 deletions fixtures/diagnostics/text_trailing_backslash_fail.hum
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module fixtures.diagnostics.text_trailing_backslash_fail

task trailing_backslash() -> Text {
why:
misuse fixture: a trailing backslash before the closing quote is a
checker error (H0638, unterminated escape)

cost:
time: O(1)
space: O(1)
check: warn

does:
return "ab\"
}
18 changes: 18 additions & 0 deletions fixtures/text_escapes_decode.hum
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
module fixtures.text_escapes_decode

task escapes_decode() -> Text {
why:
positive fixture: all four accepted escapes decode exactly once

cost:
time: O(1)
space: O(1)
check: warn

does:
let newline = "a\nb"
let tab = "c\td"
let backslash = "e\\f"
let quote = "g\"h"
return newline
}
2 changes: 2 additions & 0 deletions src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ pub(crate) enum CanonicalMalformedCause {
ListTrailingComma,
ListNonTextElement,
IntegerLiteralOutOfRange,
InvalidTextEscape,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand All @@ -180,6 +181,7 @@ pub(crate) enum CanonicalExpectedLexicalEvidence {
ListSeparatorOrClose,
TextListElement,
Int64Value,
TextEscape,
MaximumDelimiterDepth(usize),
}

Expand Down
31 changes: 28 additions & 3 deletions src/diagnostic_catalog.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,15 @@ diagnostic_causes!(
"check",
"intent_source_node",
"intent_check_route"
),
(
185,
"invalid_text_escape_v0",
INVALID_TEXT_ESCAPE,
"front_end_semantics",
"full_type_check",
"text_literal_relationship",
"text_literal_route"
)
);

Expand Down Expand Up @@ -2100,6 +2109,7 @@ const fn historical_public_ordinal(key: DiagnosticCodeKey) -> u16 {
89 => 89,
90 => 90,
91 => 91,
92 => 92,
_ => u16::MAX,
}
}
Expand Down Expand Up @@ -2840,6 +2850,15 @@ diagnostic_code_allocations!(
"front_end_semantics",
"check"
),
(
92,
INVALID_TEXT_ESCAPE,
"H0638",
"invalid text escape",
FRONT_END_SEMANTICS,
"front_end_semantics",
"full_type_check"
),
(
60,
UNCHECKED_PROSE_CONTRACT,
Expand Down Expand Up @@ -3653,6 +3672,12 @@ pub const DIAGNOSTICS: &[DiagnosticInfo] = &[
explanation: "A user task redeclares the exact `text_split` name reserved for Hum's text-splitting built-in, which would split callable identity across stages.",
repair: "Rename the user task and keep `text_split` reserved for `text_split(text: Text, sep: Text) -> List Text`.",
},
DiagnosticInfo {
code: DiagnosticCode::INVALID_TEXT_ESCAPE,
default_severity: Severity::Error,
explanation: "A text literal contains an escape sequence outside the accepted set (`\\n`, `\\t`, `\\\\`, `\\\"`) or ends with a trailing backslash.",
repair: "Use only the four accepted escapes, or remove the backslash. A literal backslash is written `\\\\`.",
},
];

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -4625,12 +4650,12 @@ mod tests {
#[test]
fn canonical_registry_and_checked_projections_are_valid() {
let summary = validate_static_registry().expect("canonical registry");
assert_eq!(summary.active_codes, 92);
assert_eq!(summary.active_codes, 93);
assert_eq!(summary.retired_codes, 0);
assert_eq!(summary.reserved_families, 3);
assert_eq!(validate_static_registry(), Ok(summary));
validate_checked_documents(&checked_documents()).expect("checked documents");
assert_eq!(DIAGNOSTIC_CAUSES.len(), 184);
assert_eq!(DIAGNOSTIC_CAUSES.len(), 185);
assert_eq!(DIAGNOSTIC_PRECEDENCE.len(), 9);
for dominant in super::H090_CAUSES {
for suppressed in super::H1401_CAUSES.iter().chain(super::H1402_CAUSES.iter()) {
Expand Down Expand Up @@ -4687,7 +4712,7 @@ mod tests {
assert!(causes.iter().all(|cause| {
cause.semantic_owner == "native_program" && cause.owning_stage == "native_admission"
}));
assert_eq!(all().len(), 92);
assert_eq!(all().len(), 93);
}

#[test]
Expand Down
6 changes: 3 additions & 3 deletions src/diagnostics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,12 @@ mod tests {
#[test]
fn registry_catalog_and_check_projections_are_semantically_equivalent() {
let catalog = crate::diagnostic_catalog::all();
assert_eq!(catalog.len(), 92);
assert_eq!(catalog.len(), 93);

let text = diagnostics_text();
assert!(text.starts_with("Hum diagnostics (92 codes)\n"));
assert!(text.starts_with("Hum diagnostics (93 codes)\n"));
let json = diagnostics_json();
assert!(json.contains("\"count\": 92"));
assert!(json.contains("\"count\": 93"));

for info in catalog {
let text_row = format!(
Expand Down
Loading
Loading