diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 613b930..2cdb61d 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -1698,6 +1698,7 @@ pub(crate) fn synth( Literal::Bool { .. } => Type::bool_(), Literal::Str { .. } => Type::str_(), Literal::Unit => Type::unit(), + Literal::Float { .. } => Type::float(), }), Term::Var { name } => { // Lookup precedence: locals → local globals → class-method @@ -2308,6 +2309,7 @@ fn type_check_pattern( Literal::Bool { .. } => Type::bool_(), Literal::Str { .. } => Type::str_(), Literal::Unit => Type::unit(), + Literal::Float { .. } => Type::float(), }; expect_eq(expected, <)?; Ok(vec![]) diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index ee6f4fb..e62843b 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -925,6 +925,7 @@ impl<'a> Emitter<'a> { let g = self.intern_string("str", value); ("ptr".to_string(), format!("@{g}")) } + Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"), }; if val_ty != lty { return Err(CodegenError::Internal(format!( @@ -1224,6 +1225,7 @@ impl<'a> Emitter<'a> { (format!("@{g}"), "ptr".into()) } Literal::Unit => ("0".into(), "i8".into()), + Literal::Float { .. } => unimplemented!("Floats milestone iter 4: codegen"), }), Term::Var { name } => { // Iter 16d: `__unreachable__` is a polymorphic bottom @@ -2358,6 +2360,7 @@ impl<'a> Emitter<'a> { Literal::Bool { .. } => Type::bool_(), Literal::Str { .. } => Type::str_(), Literal::Unit => Type::unit(), + Literal::Float { .. } => Type::float(), }), Term::Var { name } => { // Lookup precedence: extras (let-bindings introduced diff --git a/crates/ailang-core/specs/form_a.md b/crates/ailang-core/specs/form_a.md index 569b016..4b492a2 100644 --- a/crates/ailang-core/specs/form_a.md +++ b/crates/ailang-core/specs/form_a.md @@ -156,6 +156,7 @@ Atom forms (no parens): - `INT` — integer literal - `STRING` — string literal - `true`, `false` — bool literals +- `FLOAT` — IEEE-754 binary64 literal in surface form (e.g. `1.5`, `1.5e3`, `1e10`). In Form-A the literal carries the bit pattern as a 16-character lowercase hex string in `{"kind":"float","bits":""}`. Surface lex lands in iter 2 of the Floats milestone; the Form-A canonical-bytes shape is stable from iter 1. - `NAME` — variable reference (parameter, local, top-level def, or import alias) Parenthesised forms: diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index 20020d3..175c390 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -533,10 +533,42 @@ pub enum Pattern { }, } +/// Private serde helper: emits a `u64` as a 16-character lowercase +/// hex JSON string and parses the same shape back. The +/// 16-character invariant covers the full `u64` range zero-padded +/// (`format!("{:016x}", 0u64) == "0000000000000000"`, +/// `format!("{:016x}", u64::MAX) == "ffffffffffffffff"`). Used by +/// [`Literal::Float`] so float bit patterns flow through the +/// canonical-JSON *string* path — guaranteeing bit stability across +/// `serde_json` versions and surfacing NaN / ±Inf, which JSON +/// numbers cannot represent. +mod hex_u64 { + use serde::{de::Error as DeError, Deserialize, Deserializer, Serializer}; + + pub fn serialize(value: &u64, s: S) -> Result { + s.serialize_str(&format!("{:016x}", value)) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result { + let s = <&str>::deserialize(d)?; + if s.len() != 16 { + return Err(D::Error::custom(format!( + "Float bits: expected 16 hex chars, got {}", + s.len() + ))); + } + u64::from_str_radix(s, 16).map_err(D::Error::custom) + } +} + /// A literal value. /// /// The JSON discriminator is the `kind` field. `Unit` carries no -/// payload and serializes as `{"kind":"unit"}`. +/// payload and serializes as `{"kind":"unit"}`. `Float` carries a +/// `u64` IEEE-754 binary64 bit pattern, serialized via the +/// [`hex_u64`] helper as a 16-character lowercase hex string — +/// canonical-JSON bytes therefore stay bit-stable for hashing and +/// can represent NaN / ±Inf (which JSON numbers cannot). #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "kind", rename_all = "lowercase")] pub enum Literal { @@ -548,6 +580,12 @@ pub enum Literal { Str { value: String }, /// The unit value `()`. Unit, + /// IEEE-754 binary64 (LLVM `double`) carried as its bit pattern. + /// See [`hex_u64`] for the serialization rationale. + Float { + #[serde(with = "hex_u64")] + bits: u64, + }, } /// A type expression. @@ -632,6 +670,11 @@ impl Type { pub fn str_() -> Type { Type::Con { name: "Str".into(), args: vec![] } } + /// Convenience constructor for `Float` (no args). IEEE-754 + /// binary64. Parallel to [`Type::int`] / [`Type::bool_`]. + pub fn float() -> Type { + Type::Con { name: "Float".into(), args: vec![] } + } /// Iter 18a: build a `Type::Fn` with all parameter modes set to /// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This is diff --git a/crates/ailang-core/src/canonical.rs b/crates/ailang-core/src/canonical.rs index 067eb5d..3948dfa 100644 --- a/crates/ailang-core/src/canonical.rs +++ b/crates/ailang-core/src/canonical.rs @@ -132,4 +132,24 @@ mod tests { assert!(!s.contains(' ')); assert!(!s.contains('\n')); } + + /// Iter 22-floats.1 RED: a `Literal::Float` carries the IEEE-754 + /// binary64 bit pattern as a `u64`; canonical JSON encodes it as + /// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path, + /// NOT through `serde_json::Number` (which is not bit-stable for + /// floats and cannot represent NaN / ±Inf at all). + /// + /// Pinned literal: `1.5_f64` → bit pattern `0x3FF8000000000000` → + /// hex string `"3ff8000000000000"`. Sorted-key order is + /// `bits` < `kind` lexicographically. + #[test] + fn float_literal_canonical_bytes() { + use crate::ast::Literal; + let lit = Literal::Float { + bits: 0x3ff8_0000_0000_0000u64, + }; + let bytes = to_bytes(&lit); + let s = std::str::from_utf8(&bytes).unwrap(); + assert_eq!(s, r#"{"bits":"3ff8000000000000","kind":"float"}"#); + } } diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index b0eea34..66ed610 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -1058,7 +1058,10 @@ fn build_eq(scrutinee: Term, lit: &Literal) -> Term { Literal::Unit => Term::Lit { lit: Literal::Bool { value: true }, }, - Literal::Int { .. } | Literal::Bool { .. } | Literal::Str { .. } => Term::App { + Literal::Int { .. } + | Literal::Bool { .. } + | Literal::Str { .. } + | Literal::Float { .. } => Term::App { callee: Box::new(Term::Var { name: "==".to_string(), }), diff --git a/crates/ailang-core/src/pretty.rs b/crates/ailang-core/src/pretty.rs index c2e9562..732dd99 100644 --- a/crates/ailang-core/src/pretty.rs +++ b/crates/ailang-core/src/pretty.rs @@ -116,6 +116,7 @@ fn lit_to_string(l: &Literal) -> String { serde_json::to_string(value).unwrap() } Literal::Unit => "()".to_string(), + Literal::Float { bits } => format!("(float-bits 0x{:016x})", bits), } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index a7f6d5c..ab530e5 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -219,6 +219,7 @@ fn design_md_anchors_every_literal_variant() { (r#""kind": "bool""#, Literal::Bool { value: true }), (r#""kind": "str""#, Literal::Str { value: "x".into() }), (r#""kind": "unit""#, Literal::Unit), + (r#""kind": "float""#, Literal::Float { bits: 0 }), ]; for (anchor, lit) in exemplars { @@ -227,6 +228,7 @@ fn design_md_anchors_every_literal_variant() { Literal::Bool { .. } => "bool", Literal::Str { .. } => "str", Literal::Unit => "unit", + Literal::Float { .. } => "float", }; assert!( DESIGN_MD.contains(anchor), diff --git a/crates/ailang-core/tests/spec_drift.rs b/crates/ailang-core/tests/spec_drift.rs index 6e4f56d..f94e536 100644 --- a/crates/ailang-core/tests/spec_drift.rs +++ b/crates/ailang-core/tests/spec_drift.rs @@ -219,6 +219,7 @@ fn spec_mentions_every_literal_variant() { ("`true`, `false`", Literal::Bool { value: true }), ("`STRING`", Literal::Str { value: "x".into() }), ("(lit-unit)", Literal::Unit), + ("`FLOAT`", Literal::Float { bits: 0 }), ]; for (anchor, lit) in exemplars { @@ -227,6 +228,7 @@ fn spec_mentions_every_literal_variant() { Literal::Bool { .. } => "bool", Literal::Str { .. } => "str", Literal::Unit => "unit", + Literal::Float { .. } => "float", }; assert!( FORM_A_SPEC.contains(anchor), diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index 5fafc5f..3e855bb 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -914,6 +914,7 @@ fn write_lit(out: &mut String, lit: &Literal) { Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }), Literal::Str { value } => write_string_lit(out, value), Literal::Unit => out.push_str("()"), + Literal::Float { .. } => unimplemented!("Floats milestone iter 5: prose"), } } diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 6c66389..8572735 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -565,6 +565,7 @@ fn write_lit(out: &mut String, lit: &Literal) { Literal::Bool { value } => out.push_str(if *value { "true" } else { "false" }), Literal::Str { value } => write_string_lit(out, value), Literal::Unit => out.push_str("(lit-unit)"), + Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"), } } @@ -586,6 +587,7 @@ fn write_pattern(out: &mut String, p: &Pattern) { // came through the typechecker. out.push_str("(lit-unit)"); } + Literal::Float { .. } => unimplemented!("Floats milestone iter 2: surface print"), } out.push(')'); } diff --git a/docs/DESIGN.md b/docs/DESIGN.md index a594723..a5a5a09 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -1865,6 +1865,7 @@ variables of its body are captured from the enclosing scope. { "kind": "bool", "value": } { "kind": "str", "value": "" } { "kind": "unit" } +{ "kind": "float", "bits": "<16-lowercase-hex>" } ``` **`Pattern`** (the `pat` field of an `Arm`; discriminator `p`):