diff --git a/CURRENT_SEMANTICS.md b/CURRENT_SEMANTICS.md index c7d8e0ec..0bb0e6e5 100644 --- a/CURRENT_SEMANTICS.md +++ b/CURRENT_SEMANTICS.md @@ -564,7 +564,12 @@ Current status: - string literals are handled specially and flow through the current backend/runtime model - `len(str)` is supported - `str` currently behaves as an immutable view-like text type -- explicit casts between `str` and readonly `[]u8` / `[]char` are lowered through runtime helpers +- `str as []u8` produces a readonly byte view with the same `{ptr, len}` backing storage +- `[]u8 as str` produces a trusted text view with the same `{ptr, len}` backing storage and does not validate UTF-8; downstream `str` operations assume the bytes are valid UTF-8 +- explicit copy/view materialization helpers (`str_bytes`, `bytes_str`, `str_chars`) remain available +- direct allocating casts such as `str as []char` and `[]char as str` are rejected +- numeric formatting and `[]char` encoding use `to_str(value)`, not `as str` +- types that provide `fn Name::String(self) -> str` can use `value as str` - casts from `str` to mutable `[]mut u8` / `[]mut char` are rejected - the final owned mutable text type is not part of the current core language surface diff --git a/ferret_libs_dev/global.fer b/ferret_libs_dev/global.fer index 7447ae94..c7ff118a 100644 --- a/ferret_libs_dev/global.fer +++ b/ferret_libs_dev/global.fer @@ -3,7 +3,8 @@ // `str` is the built-in immutable text view type: an opaque fat pointer // `{ data *u8, len usize }`. // Assigning or passing `str` copies only this small view, not the backing bytes. -// The helper declarations below use `*str` as their current FFI ABI shape. +// The helper declarations below use references to pass view descriptors across +// the runtime FFI boundary. // String literals have type `str`. Use str_data(s) and str_len(s) to extract // the raw fields at C FFI boundaries. // @@ -64,6 +65,14 @@ fn set(value: &mut map[K]V, key: K, item: V) -> ?V; #[extern] fn compile_error(message: str) -> void; +// Converts Stringable values to text when formatting or encoding is required. +// Numeric types, str, []char, and types with String(self) -> str satisfy Stringable. +// Use `as` for zero-copy reshapes. +// Warning: `[]u8 as str` creates a trusted view and does not validate UTF-8. +// Cast arbitrary bytes only when they are already known to be valid UTF-8. +#[extern] +fn to_str(value: T) -> str; + // --------------------------------------------------------------------------- // str field accessors // @@ -74,34 +83,36 @@ fn compile_error(message: str) -> void; // Returns the raw byte pointer that backs s. // Valid only as long as s is live. Do not store without care. #[extern] -fn str_data(s: *str) -> ^void; +fn str_data(s: &str) -> ^void; // Returns the byte length of s (number of UTF-8 code units / bytes). #[extern] -fn str_len(s: *str) -> usize; +fn str_len(s: &str) -> usize; // Copies the UTF-8 bytes of s into a fresh []u8 slice. +// Use `s as []u8` when a readonly byte view is enough. #[extern] -fn str_bytes(s: *str) -> []u8; +fn str_bytes(s: &str) -> []u8; // Copies the bytes of a []u8 slice into a fresh str value. +// This is explicit because the operation allocates and trusts caller UTF-8. #[extern] -fn bytes_str(bytes: *[]u8) -> str; +fn bytes_str(bytes: &[]u8) -> str; // Decodes the UTF-8 bytes of s into a fresh []char slice. #[extern] -fn str_chars(s: *str) -> []char; +fn str_chars(s: &str) -> []char; -// Encodes a []char slice into a fresh UTF-8 str value. +// Runtime target used by to_str([]char). #[extern] -fn chars_str(chars: *[]char) -> str; +fn chars_str(chars: &[]char) -> str; // Copies s into a newly allocated NUL-terminated C string. // The returned pointer can be passed to C APIs and freed with free(). #[extern] -fn str_cstr(s: *str) -> ^void; +fn str_cstr(s: &str) -> ^void; -// Internal helpers used by numeric-to-string cast lowering. +// Runtime targets used by to_str(numeric). #[extern] fn i64_str(value: i64) -> str; diff --git a/ferret_libs_dev/std/net/http.fer b/ferret_libs_dev/std/net/http.fer index 06b4f346..9a489291 100644 --- a/ferret_libs_dev/std/net/http.fer +++ b/ferret_libs_dev/std/net/http.fer @@ -328,7 +328,7 @@ fn write_response(conn: &mut tcp::Conn, status: u16, content_type: str, body: st total = (total as i64 + (conn.Write("HTTP/1.1 ") catch |err| { return err }) as i64) as usize - total = (total as i64 + (conn.Write(status as str) catch |err| { + total = (total as i64 + (conn.Write(to_str(status)) catch |err| { return err }) as i64) as usize total = (total as i64 + (conn.Write(" ") catch |err| { @@ -340,7 +340,7 @@ fn write_response(conn: &mut tcp::Conn, status: u16, content_type: str, body: st total = (total as i64 + (conn.Write("\r\nContent-Length: ") catch |err| { return err }) as i64) as usize - total = (total as i64 + (conn.Write(len(body as []u8) as str) catch |err| { + total = (total as i64 + (conn.Write(to_str(len(body as []u8))) catch |err| { return err }) as i64) as usize total = (total as i64 + (conn.Write("\r\nContent-Type: ") catch |err| { diff --git a/internal/analysis/semantics/resolver/resolver.go b/internal/analysis/semantics/resolver/resolver.go index 735f109a..81ccb7f8 100644 --- a/internal/analysis/semantics/resolver/resolver.go +++ b/internal/analysis/semantics/resolver/resolver.go @@ -1023,7 +1023,7 @@ func (r *resolver) reportInvalidType(loc source.Location, name string) { } func isPredeclaredType(name string) bool { - return tokens.IsBuiltinType(name) || name == "Type" || name == "Comparable" + return tokens.IsBuiltinType(name) || name == "Type" || name == "Comparable" || name == "Stringable" } func (r *resolver) findOwnerModuleForSymbol(sym *symbols.Symbol) *context.Module { diff --git a/internal/analysis/semantics/typechecker/syntax_types.go b/internal/analysis/semantics/typechecker/syntax_types.go index 329000a9..ef0b07d9 100644 --- a/internal/analysis/semantics/typechecker/syntax_types.go +++ b/internal/analysis/semantics/typechecker/syntax_types.go @@ -16,46 +16,44 @@ func (c *checker) typeFromSyntax(mod *context.Module, expr ast.TypeExpr) typeinf case nil: return nil case *ast.NamedType: - if len(t.TypeArgs) > 0 && len(t.Path) == 1 { - if tokens.IsBuiltinType(t.Path[0]) { + if name, ok := unqualifiedTypeName(t); ok { + if len(t.TypeArgs) > 0 && tokens.IsBuiltinType(name) { loc := t.Loc() c.ctx.Diagnostics.Add( - diagnostics.NewError(fmt.Sprintf("type %q is not generic", t.Path[0])). + diagnostics.NewError(fmt.Sprintf("type %q is not generic", name)). WithCode(diagnostics.ErrTypeMismatch). WithPrimaryLabel(&loc, "remove type arguments from this non-generic type"), ) return typeinfo.InvalidType{} } - if _, ok := c.lookupTypeParam(t.Path[0]); ok { - loc := t.Loc() - c.ctx.Diagnostics.Add( - diagnostics.NewError(fmt.Sprintf("type parameter %q cannot take type arguments", t.Path[0])). - WithCode(diagnostics.ErrTypeMismatch). - WithPrimaryLabel(&loc, "remove type arguments from this type parameter"), - ) - return typeinfo.InvalidType{} - } - } - if len(t.Path) == 1 && tokens.IsBuiltinType(t.Path[0]) { - if t.Path[0] == "str" { - return &typeinfo.StringType{} + if tokens.IsBuiltinType(name) { + if name == "str" { + return &typeinfo.StringType{} + } + return &typeinfo.BuiltinType{Name: name} } - return &typeinfo.BuiltinType{Name: t.Path[0]} - } - if len(t.Path) == 1 { - if t.Path[0] == "Comparable" { + if constraint := predeclaredConstraintType(name); constraint != nil { if len(t.TypeArgs) > 0 { loc := t.Loc() c.ctx.Diagnostics.Add( - diagnostics.NewError(`type "Comparable" is not generic`). + diagnostics.NewError(fmt.Sprintf("type %q is not generic", name)). WithCode(diagnostics.ErrTypeMismatch). WithPrimaryLabel(&loc, "remove type arguments from this constraint"), ) return typeinfo.InvalidType{} } - return &typeinfo.ComparableConstraint{} + return constraint } - if typeParam, ok := c.lookupTypeParam(t.Path[0]); ok { + if typeParam, ok := c.lookupTypeParam(name); ok { + if len(t.TypeArgs) > 0 { + loc := t.Loc() + c.ctx.Diagnostics.Add( + diagnostics.NewError(fmt.Sprintf("type parameter %q cannot take type arguments", name)). + WithCode(diagnostics.ErrTypeMismatch). + WithPrimaryLabel(&loc, "remove type arguments from this type parameter"), + ) + return typeinfo.InvalidType{} + } return typeParam } } @@ -249,6 +247,24 @@ func (c *checker) typeFromSyntax(mod *context.Module, expr ast.TypeExpr) typeinf } } +func unqualifiedTypeName(t *ast.NamedType) (string, bool) { + if t == nil || len(t.Path) != 1 { + return "", false + } + return t.Path[0], true +} + +func predeclaredConstraintType(name string) typeinfo.Type { + switch name { + case "Comparable": + return &typeinfo.ComparableConstraint{} + case "Stringable": + return &typeinfo.StringableConstraint{} + default: + return nil + } +} + func (c *checker) selectedUnionMemberTarget(mod *context.Module, expr ast.TypeExpr) (typeinfo.Type, typeinfo.Type, bool) { named, ok := expr.(*ast.NamedType) if !ok || named == nil { @@ -284,10 +300,10 @@ func (c *checker) lookupNamedUnionMemberType(mod *context.Module, unionDecl *ast } for _, member := range unionDecl.Members { named, ok := member.(*ast.NamedType) - if !ok || named == nil || len(named.Path) != 1 { + if !ok { continue } - if named.Path[0] == name { + if memberName, ok := unqualifiedTypeName(named); ok && memberName == name { return c.typeFromSyntax(mod, member), true } } diff --git a/internal/analysis/semantics/typechecker/typechecker.go b/internal/analysis/semantics/typechecker/typechecker.go index 4eb2ecb3..87fce285 100644 --- a/internal/analysis/semantics/typechecker/typechecker.go +++ b/internal/analysis/semantics/typechecker/typechecker.go @@ -2115,6 +2115,13 @@ func (c *checker) checkTypeParamConstraintsAt(loc source.Location, params []*typ c.reportTypeMismatch(loc, constraint, actual) continue } + if _, ok := constraint.(*typeinfo.StringableConstraint); ok { + if c.isStringableType(actual) { + continue + } + c.reportTypeMismatch(loc, constraint, actual) + continue + } if c.assignable(constraint, actual) { continue } @@ -2126,6 +2133,17 @@ func (c *checker) checkTypeParamConstraintsAt(loc source.Location, params []*typ } } +func (c *checker) isStringableType(typ typeinfo.Type) bool { + base := c.underlying(typ) + if approx, ok := base.(*typeinfo.ApproxType); ok && approx != nil { + return c.isStringableType(approx.Inner) + } + return c.isStringType(typ) || + typeinfo.IsNumeric(base) || + c.isCharSliceType(typ) || + c.isStringMethodCoercion(&typeinfo.StringType{}, typ) +} + func (c *checker) checkInstantiatedGenericRequirements(call *ast.CallExpr, callee ast.Node, bindings map[*typeinfo.TypeParam]typeinfo.Type) bool { sym := c.resolvedFunctionSymbol(callee) if sym == nil { @@ -3070,14 +3088,16 @@ func (c *checker) isExplicitEnumCast(target, source typeinfo.Type) bool { } func (c *checker) isExplicitStringCast(target, source typeinfo.Type) bool { - if c.isStringType(target) && (c.isByteSliceType(source) || c.isCharSliceType(source) || typeinfo.IsNumeric(source)) { - return true - } if c.isStringMethodCoercion(target, source) { return true } - if c.isStringType(source) && (c.isByteSliceType(target) || c.isCharSliceType(target)) { - return true + if c.isStringType(source) { + targetSlice, ok := c.underlying(target).(*typeinfo.SliceType) + return ok && targetSlice != nil && !targetSlice.Mutable && typeinfo.IsBuiltinNamed(targetSlice.Inner, "u8") + } + if c.isStringType(target) { + sourceSlice, ok := c.underlying(source).(*typeinfo.SliceType) + return ok && sourceSlice != nil && !sourceSlice.Mutable && typeinfo.IsBuiltinNamed(sourceSlice.Inner, "u8") } return false } diff --git a/internal/analysis/semantics/typechecker/typechecker_test.go b/internal/analysis/semantics/typechecker/typechecker_test.go index 77bc3067..87f9cfbf 100644 --- a/internal/analysis/semantics/typechecker/typechecker_test.go +++ b/internal/analysis/semantics/typechecker/typechecker_test.go @@ -1461,7 +1461,7 @@ fn main() -> i32 { } } -func TestTypecheckerAllowsNumericToStringCast(t *testing.T) { +func TestTypecheckerRejectsNumericToStringCast(t *testing.T) { root := t.TempDir() mustWriteType(t, filepath.Join(root, "main.fer"), ` fn main() -> void { @@ -1470,6 +1470,25 @@ fn main() -> void { print(a) print(b) } +`) + + result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) + if !result.Diagnostics.HasErrors() { + t.Fatal("expected numeric-to-string cast diagnostics") + } +} + +func TestTypecheckerAllowsStringByteSliceViewCasts(t *testing.T) { + root := t.TempDir() + mustWriteType(t, filepath.Join(root, "main.fer"), ` +fn main(s: str) -> usize { + let bytes = s as []u8 + return len(bytes) +} + +fn from_bytes(bytes: []u8) -> str { + return bytes as str +} `) result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) @@ -1478,6 +1497,81 @@ fn main() -> void { } } +func TestTypecheckerRejectsAllocatingStringCasts(t *testing.T) { + cases := []string{ + ` +fn main(chars: []char) -> str { + return chars as str +} +`, + ` +fn main(s: str) -> []char { + return s as []char +} +`, + } + for _, src := range cases { + root := t.TempDir() + mustWriteType(t, filepath.Join(root, "main.fer"), src) + result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) + if !result.Diagnostics.HasErrors() { + t.Fatalf("expected allocating string cast diagnostic for source:\n%s", src) + } + } +} + +func TestTypecheckerAllowsToStrStringableConversions(t *testing.T) { + root := t.TempDir() + mustWriteType(t, filepath.Join(root, "main.fer"), ` +type Name struct {} + +fn Name::String(self) -> str { + return "name" +} + +fn main(chars: []char, n: Name) -> str { + let a = to_str(42) + let b = to_str(1.5) + let c = to_str(chars) + let d = to_str(n) + return a +} +`) + + result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) + if result.Diagnostics.HasErrors() { + t.Fatalf("unexpected diagnostics: %#v", result.Diagnostics.Diagnostics()) + } +} + +func TestTypecheckerAllowsApproxNumericToStr(t *testing.T) { + root := t.TempDir() + mustWriteType(t, filepath.Join(root, "main.fer"), ` +fn main() -> str { + return to_str<~i32>(1) +} +`) + + result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) + if result.Diagnostics.HasErrors() { + t.Fatalf("unexpected diagnostics: %#v", result.Diagnostics.Diagnostics()) + } +} + +func TestTypecheckerRejectsToStrForByteSliceReshape(t *testing.T) { + root := t.TempDir() + mustWriteType(t, filepath.Join(root, "main.fer"), ` +fn main(bytes: []u8) -> str { + return to_str(bytes) +} +`) + + result := compiler.New(root, ".fer", diagnostics.NewDiagnosticBag("")).ParseEntry(filepath.Join(root, "main.fer")) + if !result.Diagnostics.HasErrors() { + t.Fatal("expected to_str diagnostic for byte slice") + } +} + func TestTypecheckerRejectsMutationThroughStringSliceView(t *testing.T) { root := t.TempDir() mustWriteType(t, filepath.Join(root, "main.fer"), ` @@ -1842,7 +1936,7 @@ type Name struct { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } fn main() -> str { @@ -1902,7 +1996,7 @@ type Name struct { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } fn main() -> i32 { diff --git a/internal/analysis/semantics/typeinfo/types.go b/internal/analysis/semantics/typeinfo/types.go index 2dc4940b..af172703 100644 --- a/internal/analysis/semantics/typeinfo/types.go +++ b/internal/analysis/semantics/typeinfo/types.go @@ -44,6 +44,10 @@ type ComparableConstraint struct{} func (*ComparableConstraint) String() string { return "Comparable" } +type StringableConstraint struct{} + +func (*StringableConstraint) String() string { return "Stringable" } + type TypeParam struct { Name string Constraint Type @@ -440,6 +444,9 @@ func Equal(a, b Type) bool { case *ComparableConstraint: _, ok := b.(*ComparableConstraint) return ok + case *StringableConstraint: + _, ok := b.(*StringableConstraint) + return ok case *TypeParam: bt, ok := b.(*TypeParam) if !ok { diff --git a/internal/backend/llvm/llvm.go b/internal/backend/llvm/llvm.go index bd45f664..5de0917c 100644 --- a/internal/backend/llvm/llvm.go +++ b/internal/backend/llvm/llvm.go @@ -3078,9 +3078,7 @@ func lowerAggregateAssign(state *moduleState, agg *aggregateLocal, value mir.Val return lowerAggregateAssign(state, agg, v.Right) } case *mir.CastValue: - srcCast := backend.UnwrapNamed(v.Left.Type()) - dstCast := backend.UnwrapNamed(v.Type()) - if isAggregateType(state, v.Type()) && isAggregateType(state, v.Left.Type()) && !isStringSliceCastPair(srcCast, dstCast) && !isSpecialAggregate(v.Left.Type()) { + if isAggregateType(state, v.Type()) && isAggregateType(state, v.Left.Type()) && !isSpecialAggregate(v.Left.Type()) { return lowerAggregateAssign(state, agg, v.Left) } if isInterfaceAggregate(v.Left.Type()) && !isInterfaceAggregate(v.Type()) && isAggregateType(state, v.Type()) { @@ -4953,9 +4951,6 @@ func lowerCast(state *moduleState, v *mir.CastValue) (string, error) { } src := backend.UnwrapNamed(v.Left.Type()) dst := backend.UnwrapNamed(v.Type()) - if call, ok, err := lowerStringSliceCast(state, src, dst, v.Left); ok || err != nil { - return call, err - } if isUnionAggregate(v.Left.Type()) { srcPtr, err := lowerUnionSource(state, v.Left) if err != nil { @@ -4989,9 +4984,6 @@ func lowerCast(state *moduleState, v *mir.CastValue) (string, error) { state.pendingLines = append(state.pendingLines, fmt.Sprintf("%s = load %s, ptr %s", tmp, irType, srcPtr)) return llvmCopyExpr(irType, tmp) } - if _, ok := dst.(*typeinfo.StringType); ok { - return lowerStringCast(state, v.Left) - } if isAggregateType(state, v.Left.Type()) && isAggregateType(state, v.Type()) { return lowerValue(state, v.Left) } @@ -5039,70 +5031,6 @@ func lowerCast(state *moduleState, v *mir.CastValue) (string, error) { return "", fmt.Errorf("unsupported cast from %s to %s", src, dst) } -func lowerStringSliceCast(state *moduleState, src, dst typeinfo.Type, value mir.Value) (string, bool, error) { - if _, ok := src.(*typeinfo.StringType); ok { - elem, ok := sliceElementBuiltin(dst) - if !ok { - return "", false, nil - } - srcPtr, err := lowerAggregateSource(state, value) - if err != nil { - return "", true, err - } - switch elem { - case "u8": - return fmt.Sprintf("call %s @ferret_global_str_bytes(ptr %s)", llvmSliceLikeType(), srcPtr), true, nil - case "char": - return fmt.Sprintf("call %s @ferret_global_str_chars(ptr %s)", llvmSliceLikeType(), srcPtr), true, nil - default: - return "", false, nil - } - } - if _, ok := dst.(*typeinfo.StringType); ok { - elem, ok := sliceElementBuiltin(src) - if !ok { - return "", false, nil - } - srcPtr, err := lowerAggregateSource(state, value) - if err != nil { - return "", true, err - } - switch elem { - case "u8": - return fmt.Sprintf("call %s @ferret_global_bytes_str(ptr %s)", llvmSliceLikeType(), srcPtr), true, nil - case "char": - return fmt.Sprintf("call %s @ferret_global_chars_str(ptr %s)", llvmSliceLikeType(), srcPtr), true, nil - default: - return "", false, nil - } - } - return "", false, nil -} - -func isStringSliceCastPair(src, dst typeinfo.Type) bool { - if _, ok := src.(*typeinfo.StringType); ok { - elem, ok := sliceElementBuiltin(dst) - return ok && (elem == "u8" || elem == "char") - } - if _, ok := dst.(*typeinfo.StringType); ok { - elem, ok := sliceElementBuiltin(src) - return ok && (elem == "u8" || elem == "char") - } - return false -} - -func sliceElementBuiltin(typ typeinfo.Type) (string, bool) { - sliceType, ok := typ.(*typeinfo.SliceType) - if !ok || sliceType == nil { - return "", false - } - builtin, ok := backend.UnwrapNamed(sliceType.Inner).(*typeinfo.BuiltinType) - if !ok || builtin == nil { - return "", false - } - return builtin.Name, true -} - func lowerUnionSource(state *moduleState, value mir.Value) (string, error) { switch v := value.(type) { case *mir.UnaryValue: @@ -5230,45 +5158,6 @@ func llvmUnionMemberTypes(info *layout.UnionLayout) []typeinfo.Type { return out } -func lowerStringCast(state *moduleState, value mir.Value) (string, error) { - srcVal, err := lowerValue(state, value) - if err != nil { - return "", err - } - src := backend.UnwrapNamed(value.Type()) - srcBuiltin, ok := src.(*typeinfo.BuiltinType) - if !ok { - return "", fmt.Errorf("unsupported string cast source %s", src) - } - switch srcBuiltin.Name { - case "i8", "i16", "i32": - castExpr, _ := llvmIntCastOp(nil, srcBuiltin.Name, "i64", srcVal) - return fmt.Sprintf("call %s @ferret_global_i64_str(%s)", llvmSliceLikeType(), operandWithTemp(state, "i64", castExpr)), nil - case "i64", "isize": - if srcBuiltin.Name == "isize" { - castExpr, _ := llvmIntCastOp(nil, srcBuiltin.Name, "i64", srcVal) - return fmt.Sprintf("call %s @ferret_global_i64_str(%s)", llvmSliceLikeType(), operandWithTemp(state, "i64", castExpr)), nil - } - return fmt.Sprintf("call %s @ferret_global_i64_str(i64 %s)", llvmSliceLikeType(), srcVal), nil - case "u8", "u16", "u32", "bool", "char": - castExpr, _ := llvmIntCastOp(nil, srcBuiltin.Name, "u64", srcVal) - return fmt.Sprintf("call %s @ferret_global_u64_str(%s)", llvmSliceLikeType(), operandWithTemp(state, "i64", castExpr)), nil - case "u64", "usize": - if srcBuiltin.Name == "usize" { - castExpr, _ := llvmIntCastOp(nil, srcBuiltin.Name, "u64", srcVal) - return fmt.Sprintf("call %s @ferret_global_u64_str(%s)", llvmSliceLikeType(), operandWithTemp(state, "i64", castExpr)), nil - } - return fmt.Sprintf("call %s @ferret_global_u64_str(i64 %s)", llvmSliceLikeType(), srcVal), nil - case "f32": - castExpr, _ := llvmFloatCastOp("f32", "f64", srcVal) - return fmt.Sprintf("call %s @ferret_global_f64_str(%s)", llvmSliceLikeType(), operandWithTemp(state, "double", castExpr)), nil - case "f64": - return fmt.Sprintf("call %s @ferret_global_f64_str(double %s)", llvmSliceLikeType(), srcVal), nil - default: - return "", fmt.Errorf("unsupported string cast source %s", srcBuiltin.Name) - } -} - func operandWithTemp(state *moduleState, irType, expr string) string { if !strings.Contains(expr, " ") || strings.HasPrefix(expr, "%") || strings.HasPrefix(expr, "@") || expr == "null" { return irType + " " + expr diff --git a/internal/backend/llvm/lower_test.go b/internal/backend/llvm/lower_test.go index c2242cd0..15238097 100644 --- a/internal/backend/llvm/lower_test.go +++ b/internal/backend/llvm/lower_test.go @@ -128,7 +128,7 @@ type Name struct { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } fn main() -> str { @@ -177,7 +177,7 @@ fn Origin() -> Name { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } `) mustWrite(t, filepath.Join(root, "main.fer"), ` @@ -305,7 +305,7 @@ type Name struct { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } let GlobalName: Name = .{ .value = 1 } @@ -840,7 +840,7 @@ type Name struct { } fn Name::String(self) -> str { - return self.value as str + return to_str(self.value) } fn main() -> void { @@ -1491,13 +1491,14 @@ fn main() -> i32 { } } -func TestLowerStringSliceCastsToLLVM(t *testing.T) { +func TestLowerStringByteSliceViewCastToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` -fn main(s: str) -> str { +fn main(s: str) -> usize { let bytes = s as []u8 let text = bytes as str - return text + let again = text as []u8 + return len(again) } `) result := compiler.ParsePath(filepath.Join(root, "main.fer")) @@ -1513,24 +1514,21 @@ fn main(s: str) -> str { t.Fatalf("lower llvm: %v", err) } text := artifact.Text - for _, want := range []string{ - "declare { ptr, i64 } @ferret_global_str_bytes(ptr)", - "declare { ptr, i64 } @ferret_global_bytes_str(ptr)", + for _, unwanted := range []string{ "call { ptr, i64 } @ferret_global_str_bytes(", "call { ptr, i64 } @ferret_global_bytes_str(", } { - if !strings.Contains(text, want) { - t.Fatalf("expected %q in llvm output:\n%s", want, text) + if strings.Contains(text, unwanted) { + t.Fatalf("did not expect allocating string conversion %q in llvm output:\n%s", unwanted, text) } } } -func TestLowerStringCharSliceCastsToLLVM(t *testing.T) { +func TestLowerExplicitStringCharSliceConversionsToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` fn main(s: str) -> str { - let chars = s as []char - let text = chars as str + let text = to_str(str_chars(&s)) return text } `) @@ -1559,11 +1557,11 @@ fn main(s: str) -> str { } } -func TestLowerNumericToStringCastToLLVM(t *testing.T) { +func TestLowerExplicitNumericStringConversionToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` fn main() -> void { - let text = 42 as str + let text = to_str(42) print(text) } `) @@ -1591,6 +1589,37 @@ fn main() -> void { } } +func TestLowerApproxNumericStringConversionToLLVM(t *testing.T) { + root := t.TempDir() + mustWrite(t, filepath.Join(root, "main.fer"), ` +fn main() -> void { + let text = to_str<~i32>(42) + print(text) +} +`) + result := compiler.ParsePath(filepath.Join(root, "main.fer")) + if result.Diagnostics.HasErrors() { + t.Fatalf("unexpected diagnostics: %#v", result.Diagnostics.Diagnostics()) + } + lowerer, err := registry.New(backend.TargetLLVM) + if err != nil { + t.Fatalf("unexpected llvm error: %v", err) + } + artifact, err := lowerer.LowerModule(testUnit(result)) + if err != nil { + t.Fatalf("lower llvm: %v", err) + } + text := artifact.Text + for _, want := range []string{ + "declare { ptr, i64 } @ferret_global_i64_str(i64)", + "call { ptr, i64 } @ferret_global_i64_str(", + } { + if !strings.Contains(text, want) { + t.Fatalf("expected %q in llvm output:\n%s", want, text) + } + } +} + func TestLowerStringMethodCoercionToStringToLLVM(t *testing.T) { root := t.TempDir() mustWrite(t, filepath.Join(root, "main.fer"), ` diff --git a/internal/ir/mir/lower.go b/internal/ir/mir/lower.go index 6a9963c4..6e9501f7 100644 --- a/internal/ir/mir/lower.go +++ b/internal/ir/mir/lower.go @@ -518,6 +518,9 @@ func lowerValue(lowerCtx *lowerContext, expr hir.Expr) Value { if builtinLen, ok := lowerBuiltinLenCallValue(lowerCtx, e); ok { return builtinLen } + if builtinToStr, ok := lowerBuiltinToStrCallValue(lowerCtx, e); ok { + return builtinToStr + } stringifyAnyArgs := lowerIsPrintLikeAnyCall(lowerCtx, e.Callee) // Normalize method calls (instance.Method(...)) to direct function calls // by prepending the receiver as the first argument, e.g. p.Len2() → Len2(p). @@ -1032,6 +1035,62 @@ func lowerBuiltinLenCallValue(c *lowerContext, call *hir.CallExpr) (Value, bool) } } +func lowerBuiltinToStrCallValue(c *lowerContext, call *hir.CallExpr) (Value, bool) { + if call == nil || len(call.Args) != 1 || !lowerIsForeignToStrCall(c, call.Callee) { + return nil, false + } + arg := call.Args[0] + argType := arg.Type() + result := &typeinfo.StringType{} + if _, ok := argType.(*typeinfo.StringType); ok { + return lowerValue(c, arg), true + } + if c != nil { + numericType := argType + if approx, ok := numericType.(*typeinfo.ApproxType); ok && approx != nil { + numericType = approx.Inner + } + if family, _, ok := typeinfo.NumericInfo(numericType); ok { + targetType := typeinfo.Type(&typeinfo.BuiltinType{Name: "i64"}) + linkName := "ferret_global_i64_str" + if family == typeinfo.NumericUnsigned { + targetType = &typeinfo.BuiltinType{Name: "u64"} + linkName = "ferret_global_u64_str" + } else if family == typeinfo.NumericFloat { + targetType = &typeinfo.BuiltinType{Name: "f64"} + linkName = "ferret_global_f64_str" + } + return &CallValue{ + baseValue: baseValue{Location: call.Loc(), ExprType: result}, + Callee: &NameValue{ + baseValue: baseValue{Location: call.Callee.Loc(), ExprType: &typeinfo.FuncType{Result: result}}, + Path: []string{"global", "to_str"}, + LinkName: linkName, + }, + Args: []Value{&CastValue{ + baseValue: baseValue{Location: arg.Loc(), ExprType: targetType}, + Left: lowerValue(c, arg), + }}, + }, true + } + } + if slice, ok := argType.(*typeinfo.SliceType); ok && slice != nil && typeinfo.IsBuiltinNamed(slice.Inner, "char") { + return &CallValue{ + baseValue: baseValue{Location: call.Loc(), ExprType: result}, + Callee: &NameValue{ + baseValue: baseValue{Location: call.Callee.Loc(), ExprType: &typeinfo.FuncType{Result: result}}, + Path: []string{"global", "to_str"}, + LinkName: "ferret_global_chars_str", + }, + Args: []Value{lowerValue(c, arg)}, + }, true + } + if value, ok := lowerStringMethodCoercion(c, arg, result); ok { + return value, true + } + return nil, false +} + func lowerCallFuncType(callee hir.Expr) *typeinfo.FuncType { if typed, ok := callee.Type().(*typeinfo.FuncType); ok { return typed @@ -1191,6 +1250,21 @@ func lowerIsForeignLenCall(c *lowerContext, callee hir.Expr) bool { return ok && fn != nil && fn.IsExtern } +func lowerIsForeignToStrCall(c *lowerContext, callee hir.Expr) bool { + if c == nil || callee == nil || callee.SourceExpr() == nil { + return false + } + resolution, ok := c.lookupResolution(callee.SourceExpr()) + if !ok || resolution.Kind != binding.ResolutionSymbol || resolution.Symbol == nil { + return false + } + if resolution.Symbol.Name != "to_str" { + return false + } + fn, ok := resolution.Symbol.Node.(*ast.FuncDecl) + return ok && fn != nil && fn.IsExtern +} + func lowerIsPrintLikeAnyCall(c *lowerContext, callee hir.Expr) bool { if c == nil || callee == nil || callee.SourceExpr() == nil { return false diff --git a/internal/ir/mir/simplify_test.go b/internal/ir/mir/simplify_test.go index 0018b479..2d4a2602 100644 --- a/internal/ir/mir/simplify_test.go +++ b/internal/ir/mir/simplify_test.go @@ -197,7 +197,7 @@ type Name struct { } fn Name::String(self) -> str { - return 1 as str + return to_str(1) } fn main() -> i32 { diff --git a/tests/repro/map_builtin_probe.fer b/tests/repro/map_builtin_probe.fer index 213460cc..95997d36 100644 --- a/tests/repro/map_builtin_probe.fer +++ b/tests/repro/map_builtin_probe.fer @@ -1,13 +1,13 @@ fn main() -> void { let mut nums = map[str]i32{"one" => 1} - println(size(&nums) as str) - println((get(&nums, "one") ?? 0) as str) - println((set(&mut nums, "one", 2) ?? 0) as str) - println((get(&nums, "one") ?? 0) as str) + println(to_str(size(&nums))) + println(to_str(get(&nums, "one") ?? 0)) + println(to_str(set(&mut nums, "one", 2) ?? 0)) + println(to_str(get(&nums, "one") ?? 0)) println("capacity: ", cap(&nums)) let mut words = map[str]str{"a" => "alpha"} - println(size(&words) as str) + println(to_str(size(&words))) if get(&words, "a") is str { println("true") } else { diff --git a/tests/repro/map_field_mut_probe.fer b/tests/repro/map_field_mut_probe.fer index aad88c9e..f00f21ec 100644 --- a/tests/repro/map_field_mut_probe.fer +++ b/tests/repro/map_field_mut_probe.fer @@ -9,7 +9,7 @@ fn Holder::Add(&mut self, key: str, value: i32) -> void { fn main() -> void { let mut holder: Holder = .{ .routes = {} } holder.Add("x", 1) - println("size=", size(&holder.routes) as str) + println("size=", to_str(size(&holder.routes))) if get(&holder.routes, "x") is i32 { println("found") } else { diff --git a/tests/repro/map_index_missing_panic.fer b/tests/repro/map_index_missing_panic.fer index 966b9d4c..1de85037 100644 --- a/tests/repro/map_index_missing_panic.fer +++ b/tests/repro/map_index_missing_panic.fer @@ -1,4 +1,4 @@ fn main() -> void { let values = map[str]i32{"one" => 1} - println(values["missing"] as str) + println(to_str(values["missing"])) } diff --git a/tests/repro/map_index_probe.fer b/tests/repro/map_index_probe.fer index f5d420c0..9e422ce7 100644 --- a/tests/repro/map_index_probe.fer +++ b/tests/repro/map_index_probe.fer @@ -1,8 +1,8 @@ fn main() -> void { let mut values = map[str]i32{"one" => 1} - println(values["one"] as str) + println(to_str(values["one"])) values["one"] = 2 - println(values["one"] as str) + println(to_str(values["one"])) let mut words = map[str]str{"a" => "alpha"} println(words["a"]) diff --git a/tests/repro/tcp_client_echo_probe.fer b/tests/repro/tcp_client_echo_probe.fer index 979855c5..c20d0f76 100644 --- a/tests/repro/tcp_client_echo_probe.fer +++ b/tests/repro/tcp_client_echo_probe.fer @@ -16,7 +16,7 @@ fn main() -> void { conn.Close() return } - if len(data) == 4 && data as str == "ping" { + if len(data) == 4 && (data as str) == "ping" { print("tcp-client-ok") conn.Close() return diff --git a/tests/repro/tcp_conn_controls_probe.fer b/tests/repro/tcp_conn_controls_probe.fer index 36bc5d33..c99a019d 100644 --- a/tests/repro/tcp_conn_controls_probe.fer +++ b/tests/repro/tcp_conn_controls_probe.fer @@ -51,7 +51,7 @@ fn main() -> void { conn.Close() return } - if len(data) == 4 && data as str == "pong" { + if len(data) == 4 && (data as str) == "pong" { print("tcp-conn-controls-ok") conn.Close() return diff --git a/tests/repro/tcp_listener_echo_probe.fer b/tests/repro/tcp_listener_echo_probe.fer index a181f0bd..2c9b19a6 100644 --- a/tests/repro/tcp_listener_echo_probe.fer +++ b/tests/repro/tcp_listener_echo_probe.fer @@ -17,7 +17,7 @@ fn main() -> void { listener.Close() return } - if len(data) == 4 && data as str == "ping" { + if len(data) == 4 && (data as str) == "ping" { _ = io::Write(conn, "pong") catch |err| { print(err) conn.Close() diff --git a/tests/semma/io_buffer.fer b/tests/semma/io_buffer.fer index dad1d416..ae5ec59a 100644 --- a/tests/semma/io_buffer.fer +++ b/tests/semma/io_buffer.fer @@ -27,9 +27,9 @@ fn main() -> void { if len(first) == 5 && len(second) == 7 && len(last) == 0 && - first as str == "Hello" && - second as str == ", world" && - last as str == "" && + (first as str) == "Hello" && + (second as str) == ", world" && + (last as str) == "" && buf.AsStr() == "Hello, world" { print("io-buffer-ok") buf.Release() diff --git a/tests/semma/io_file_write.fer b/tests/semma/io_file_write.fer index 3545e56c..2e6015f4 100644 --- a/tests/semma/io_file_write.fer +++ b/tests/semma/io_file_write.fer @@ -13,7 +13,7 @@ fn main() -> void { print(err) return } - _ = io::Write(file, 132245 as str) catch |err| { + _ = io::Write(file, to_str(132245)) catch |err| { print(err) return } diff --git a/tests/semma/str_ready.fer b/tests/semma/str_ready.fer index 99457beb..2f64c1c8 100644 --- a/tests/semma/str_ready.fer +++ b/tests/semma/str_ready.fer @@ -1,9 +1,9 @@ fn main() -> void { let s: str = "ok" let bytes = s as []u8 - let chars = s as []char + let chars = str_chars(&s) let roundtrip_bytes = bytes as str - let roundtrip_chars = chars as str + let roundtrip_chars = to_str(chars) if len(s) == len(bytes) && len(roundtrip_bytes) == len(s) && roundtrip_chars == s { print("str-ready-ok") return diff --git a/tests/semma/string_index.fer b/tests/semma/string_index.fer index 2b441e7e..749cd22a 100644 --- a/tests/semma/string_index.fer +++ b/tests/semma/string_index.fer @@ -12,7 +12,7 @@ fn array_len_ref(values: &[3]i32) -> usize { fn main() -> void { let s: str = "hé🙂" - let chars = s as []char + let chars = str_chars(&s) print(len(&s)) let nums: [3]i32 = [3]i32{1, 2, 3} if len(chars) == 3 && str_len_ref(&s) == 7 && slice_len_ref(&chars) == 3 && array_len_ref(&nums) == 3 && s[1] == chars[1] && s[2] == chars[2] { diff --git a/tests/semma/string_realworld_ok.fer b/tests/semma/string_realworld_ok.fer index f12e1a07..660ef8ec 100644 --- a/tests/semma/string_realworld_ok.fer +++ b/tests/semma/string_realworld_ok.fer @@ -20,7 +20,7 @@ fn main() -> void { let roundtrip = normalize(greeting) let bytes = roundtrip as []u8 - let chars = roundtrip as []char + let chars = str_chars(&roundtrip) let expected: [3]char = [3]char{'h', 'é', '🙂'} let raw: [2]u8 = [2]u8{b'h', b'i'}