diff --git a/COMPILER_CLEANUP.md b/COMPILER_CLEANUP.md deleted file mode 100644 index 509ca5c..0000000 --- a/COMPILER_CLEANUP.md +++ /dev/null @@ -1,49 +0,0 @@ -# Compiler Cleanup - -This pass is complete. - -The goal was to remove the worst architectural accidents in `capc/` without -turning the compiler into a rewrite project. - -## What Landed - -- stable expression identity and an explicit desugar pass are already in place -- lowering now has real lexical scopes instead of fake scope helpers -- parser, checker, monomorphization support, and codegen helpers are split by - concern instead of living in a few giant files -- multi-file diagnostics now attach the correct source file for imported-module - parse and type-check failures, and the driver has the plumbing needed for - codegen diagnostics to do the same -- runtime-backed stdlib functions now come from one shared runtime-binding - registry instead of a stringly boolean table in one place and a separate - signature registry in another -- the immediate MIR question is settled for now: do not add a full MIR/CFG - layer yet; if codegen needs another normalization step, start with a smaller - control-flow-normalized HIR pass first - -## Final Status - -- [x] Stable expression identity and explicit desugaring -- [x] Real lowering scopes -- [x] Match / `try` control-flow cleanup -- [x] `typeck` split by concern -- [x] Parser split by syntactic domain -- [x] Monomorphization helper split -- [x] Codegen helper split -- [x] Multi-file diagnostics for imported-module compiler errors -- [x] Shared runtime interface registry -- [x] Decision on MIR vs narrower normalization - -## Result - -The compiler is still direct, but the worst remaining problems are now -deliberate tradeoffs instead of accidental ones: - -- diagnostics are no longer effectively entry-file-only -- runtime-backed stdlib functions have one source of truth -- the next control-flow step is bounded and explicit instead of “probably add - MIR someday” - -## Verification - -- `PATH="$HOME/.cargo/bin:$PATH" cargo test -p capc` diff --git a/CURRENT_STATUS.md b/CURRENT_STATUS.md deleted file mode 100644 index 32d4ea8..0000000 --- a/CURRENT_STATUS.md +++ /dev/null @@ -1,90 +0,0 @@ -# Current status - -This file is intended to reflect the current compiler/stdlib behavior in the -repository. It is based on code and tests, not older design docs. - -## Language surface - -- Modules declare a package safety level (safe/unsafe) and a module path. -- Items: functions, extern functions, structs, enums, and impl blocks. -- Structs can be opaque, linear, copy, and/or capability types. -- Enums support payload variants. -- Generics exist on functions, structs, enums, and impl blocks and are - monomorphized during compilation. -- Statements: let, assign, defer, return, break, continue, if/else, while, - for (range syntax), and expression statements. -- Expressions: literals, paths, calls, method calls, field access, indexing, - struct literals, unary/binary ops, match, try (? operator), and grouping. - -## Types and ownership - -- Built-in types: i32, i64, u32, u64, u8, bool, unit, never. -- `string` is a stdlib struct (a view over bytes), not a compiler builtin. -- Pointers (`*T`) and borrows (`&T`) are supported in the type system. -- Plain data is unrestricted by default. -- `opaque struct` and `capability struct` are the main move-tracked categories. -- Structs/enums become move-tracked by containment when they contain - move-tracked fields. -- Borrows are deliberately narrow: refs cannot be stored in structs/enums or - returned, and ref locals must be initialized from another local. -- Unsuffixed integer literals type as i32; suffixed integer literals currently - support `u8`, `i64`, and `u64`. Char literals are `u8`. - -## Standard library and runtime - -- Stdlib modules live under `stdlib/sys` and include: args, buffer, bytes, - console, fs, io, math, mem, net, option, result, stdin, string, system, - unsafe_ptr, vec. -- Many sys::* functions are runtime intrinsics; their `.cap` bodies are stubs - and are replaced at codegen time. The intrinsic registry is in - `capc/src/codegen/intrinsics.rs`. -- Runtime-backed intrinsics currently include: - - RootCap minting for console, fs/filesystem, alloc, args, stdin, net. - - Console printing and assert. - - Filesystem ops (read/list/exists/open/close/join) and read handles. - - Net ops (listen/connect/accept/read/write/close). - - Args and stdin accessors. - - Buffer/Alloc malloc/free/casts. - - Math wrap helpers and byte whitespace checks. - -## Current capability algebra - -- Reusable use operations borrow the capability/resource where possible: - - `ReadFS.read_to_string/read_bytes/list_dir/exists` - - `Dir.read_to_string/read_bytes/list_dir/exists` - - `Stdin.read_to_string` - - `TcpConn.read_to_string/read/write` - - `TcpListener.accept` -- Attenuation operations still consume the stronger capability: - - `Filesystem.root_dir` - - `Dir.subdir` -- Child handles remain linear where appropriate: - - `FileRead` - - `TcpConn` -- On move-tracked capabilities, borrowed receivers can return fresh linear - child capabilities, but they cannot return reusable capabilities. This is - why `Dir.open_read` can borrow `Dir`, while attenuation like `Dir.subdir` - still consumes `self`. -- Deliberately copyable capabilities currently include: - - `RootCap` - - `Console` - - `Args` - - `Net` - - `TcpListener` - -## ABI and codegen - -- Codegen targets native code via Cranelift. -- Non-opaque struct returns are lowered via sret out-parameters. -- Result with struct payloads is lowered with out-parameters. -- Opaque/capability types are passed as handles. - -## Known limitations and gaps - -- Inline-by-value struct returns are not implemented (sret only). -- Vec element types are restricted to u8, i32, string, or type parameters. -- Variable shadowing is not currently modeled in lowering. -- break/continue are not supported inside expression-context matches. -- String literal escapes are limited to \n, \r, \t, \\, and \". - Char literals additionally support \xNN escapes. -- No floating-point types are implemented. diff --git a/FUTURE_COMPILER_WORK.md b/FUTURE_COMPILER_WORK.md deleted file mode 100644 index 4750f81..0000000 --- a/FUTURE_COMPILER_WORK.md +++ /dev/null @@ -1,43 +0,0 @@ -# Future Compiler Work - -These items are intentionally deferred. They are real architectural projects, -not cleanup chores. - -## High-Risk Work - -- Add a real MIR / CFG layer. - Current decision: do not do this yet. If codegen needs another simplification - step, first try a smaller control-flow-normalized HIR pass that lowers - `match`, `try`, `defer`, and loop exits into a flatter form without adding a - whole new compiler IR family. - -- Change the typecheck / lowering contract. - Current decision: do not rewrite this boundary yet. The compiler is stable - enough now that the next version of this work should only happen if a - normalized-HIR pass or incremental compilation effort makes the current - side-table contract too expensive to keep. - -- Replace the current build / link pipeline. - Current decision: do not tackle this in the cleanup pass. The compiler still - shells out through `cargo` and `rustc` and writes a small Rust stub at link - time. That is architecturally awkward, but it is isolated and working. It - should become its own project if cross-compilation, packaging, reproducible - builds, or compile-time performance make it worth paying down. - -## Trigger Conditions - -Re-open the work above only if one of these becomes true: - -- codegen complexity starts growing faster than localized helper extraction can - control -- diagnostics or optimization work need an explicit CFG-level representation -- incremental compilation or separate compilation becomes a real goal -- the current shell-driven build pipeline becomes a meaningful product problem - -## Order - -If this work is reopened, the preferred order is: - -1. Try control-flow-normalized HIR before full MIR. -2. Revisit the typecheck / lowering contract only after that. -3. Tackle the build / link pipeline as a separate delivery project. diff --git a/PROBLEMS.md b/PROBLEMS.md deleted file mode 100644 index c803814..0000000 --- a/PROBLEMS.md +++ /dev/null @@ -1,100 +0,0 @@ -# Problems - -This document records the main design wrinkles in Capable as it exists today. - -It is not a roadmap. It is a statement of the places where the language is -still more complicated, more accidental, or less settled than it should be. - -## 1. The memory model is still heavy for ordinary code - -Capable has a real distinction between: - -- `string` as a non-owning view -- `Text` as owned/growable text -- `Vec` as owned storage -- explicit frees for owned heap data - -That is workable and honest, but it means routine code still needs a fair -amount of representation awareness. The language is simpler than Rust here, but -it is still not especially lightweight. - -## 2. The allocator story is still mixed - -Capable now has both: - -- explicit `Alloc` -- a default-first stdlib surface - -That is a much better default than before, but the model is not fully settled. -Allocation is still part resource handle, part policy hook, and the stdlib -still carries duplicated `_with_alloc` forms. - -The language should eventually make this story crisp: - -- default allocator for ordinary safe code -- explicit allocator for low-level or budgeted code - -Until then, the stdlib will keep carrying duplicated APIs. - -## 3. Expression and statement control flow are still somewhat brittle - -Recent work made `let ... else`, `try let`, and `try ... else` viable, but it -also showed that control-flow behavior was not fully uniform across parser, -typechecker, and codegen. - -The language now supports these forms, but this area still needs discipline. -If more expression-oriented control-flow is added casually, complexity will -rise fast. - -## 4. Traits and generics exist without a fully settled place in the language story - -Traits and generics work, and they are useful, but they are not clearly part of -Capable's core identity. - -The main value proposition of the language is: - -- explicit authority -- small resource model -- predictable systems code - -Traits and generic abstraction can help, but they can also distract from that -core if they keep expanding before the simpler story is fully stable. - -## 5. The language boundary is split across compiler, stdlib stubs, and runtime intrinsics - -A significant part of the real language surface is defined by the combination -of: - -- stdlib `.cap` declarations -- intrinsic registration -- runtime handle tables and host functions - -That is a reasonable implementation technique, but it makes some language -behavior feel more accidental than intentional. It is easy for docs, stubs, and -runtime behavior to drift unless they are kept tightly aligned. - -## 6. Remote capability delegation is still unsolved work - -The local model is much clearer now, but the remote story remains a separate -future project. - -That is the right scope decision, but it means Capable still does not yet have -a real end-to-end answer for: - -- delegated authority over the network -- revocation and lease semantics -- typed remote proxies -- constrained remote agents - -The current answer is architectural intent, not shipped behavior. - -## Bottom Line - -Capable's biggest remaining problems are no longer "missing features". - -They are mostly about choosing and enforcing a smaller number of intended ways -to write code: - -- a lighter ordinary-data story -- one coherent allocation story -- a tighter boundary around abstraction features diff --git a/REMOTE_CAPS_RFC.md b/REMOTE_CAPS_RFC.md deleted file mode 100644 index 5f3b1ce..0000000 --- a/REMOTE_CAPS_RFC.md +++ /dev/null @@ -1,161 +0,0 @@ -# Remote Capability Delegation RFC - -This document describes a future initiative: delegating attenuated capabilities -to remote workers or agents over the network. - -It is intentionally separate from the current local-language docs. The local -model should be stabilized first. Remote delegation builds on that model; it -should not distort the scope of the local cleanup work. - -## Goal - -Allow a local process to delegate explicit, attenuated authority to a remote -agent while preserving Capable's core guarantee: - -> safe code can only exercise authority it was explicitly given - -## Non-Goals - -Remote delegation is not: - -- serializing local runtime handles -- exporting `RootCap` -- turning the network into ambient authority -- pretending remote calls are local calls -- claiming hostile multi-tenant isolation by default - -## Why This Is Separate Work - -The local cleanup plan is mostly: - -- docs and tutorial cleanup -- stdlib API cleanup -- local capability algebra clarification - -Remote delegation adds a distinct class of work: - -- authenticated sessions -- lease tables -- typed proxy capabilities -- revocation and expiry -- protocol design -- audit logging - -That is a real runtime/protocol project, not a minor extension of the local -cleanup. - -## Recommended Architecture - -The first design should be explicit and typed: - -- a local broker owns real local capabilities -- the broker exports only explicitly delegated capabilities -- the remote side receives typed proxy capabilities, not raw local handles -- proxy method calls are RPCs back to the broker -- the broker revalidates lease and policy on every call - -This should look like authority delegation, not distributed shared memory. - -## Core Concepts - -- Broker: trusted local runtime that owns local capabilities. -- Session: authenticated remote relationship with the broker. -- Lease: revocable exported authority bound to a session. -- Proxy capability: typed remote handle that forwards to a lease. - -## Security Rules for v1 - -- `RootCap` is non-exportable. -- Export must be explicit and typed. -- Every lease should carry: - - capability kind - - policy payload - - session binding - - expiry and revocation state -- Every remote call should revalidate: - - session identity - - lease existence - - lease cookie/generation - - policy constraints -- Remote proxies should never be castable to local capability types. - -## Scope for v1 - -Start with a very small set of exportable capabilities: - -- remote read-only filesystem -- remote command execution with an explicit command/profile allowlist -- remote console/log sink -- optionally remote workspace file read/write as a separate capability family - -Do not start with: - -- generic capability serialization -- remote `RootCap` -- arbitrary user-defined capability export -- distributed GC -- async/futures machinery - -## Why Typed Proxies First - -Typed proxies keep four things explicit: - -- authority -- latency -- fallibility -- auditability - -That matters for agent workflows. If an LLM-driven worker can call a remote -filesystem or exec capability, those calls should remain visibly remote and -explicitly delegated. - -## Trust-Boundary Warning - -If multiple users or agents can steer the same session, they share the -delegated authority of that session. - -The broker model is an authority-delegation mechanism. It is not, by itself, a -complete hostile multi-tenant sandbox. - -## Suggested Future Phases - -### Phase A: Stabilize the local model - -Complete the local cleanup and address the issues in [CURRENT_STATUS.md](./CURRENT_STATUS.md) -and [PROBLEMS.md](./PROBLEMS.md) first. - -### Phase B: Design `sys::remote` - -- define the safe surface API -- decide exported capability families -- decide proxy error model -- decide session lifecycle model - -### Phase C: Build a broker MVP - -- implement authenticated sessions -- implement lease tables -- implement typed proxy calls for a tiny capability set -- add revocation, expiry, and audit logging - -### Phase D: Dogfood on agent workflows - -Use the broker to support constrained remote workers: - -- a code-writing agent with only a workspace capability -- a test-running agent with a restricted exec profile -- a docs/indexing agent with read-only filesystem access - -The goal is to prove the authority model on real workflows before widening the -protocol. - -## Acceptance Criteria - -Remote delegation is only ready to ship if: - -- local capability semantics are already crisp -- delegated authority is always narrower than local root authority -- revoked or expired leases fail closed -- remote proxies cannot be forged or widened -- policy and session checks happen on every call -- the resulting system is auditable enough to reason about agent authority diff --git a/capc/tests/run.rs b/capc/tests/run.rs index 2287bf8..97603df 100644 --- a/capc/tests/run.rs +++ b/capc/tests/run.rs @@ -450,6 +450,20 @@ fn run_result_construct() { assert!(stdout.contains("7"), "stdout was: {stdout:?}"); } +#[test] +fn run_result_helpers() { + let out_dir = make_out_dir("result_helpers"); + let out_dir = out_dir.to_str().expect("utf8 out dir"); + let (code, stdout, _stderr) = run_capc(&[ + "run", + "--out-dir", + out_dir, + "tests/programs/result_helpers.cap", + ]); + assert_eq!(code, 0); + assert!(stdout.contains("result helpers ok"), "stdout was: {stdout:?}"); +} + #[test] fn run_malloc_demo() { let out_dir = make_out_dir("malloc_demo"); diff --git a/capc/tests/typecheck.rs b/capc/tests/typecheck.rs index abb7e43..65b9c65 100644 --- a/capc/tests/typecheck.rs +++ b/capc/tests/typecheck.rs @@ -256,28 +256,6 @@ fn typecheck_match_result_non_exhaustive_fails() { assert!(err.to_string().contains("non-exhaustive match on Result")); } -#[test] -fn typecheck_result_ok_helper_removed() { - let source = load_program("should_fail_result_ok_removed.cap"); - let module = parse_module(&source).expect("parse module"); - let stdlib = load_stdlib().expect("load stdlib"); - let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); - assert!(err - .to_string() - .contains("unknown method `sys.result.Result__ok`")); -} - -#[test] -fn typecheck_result_unwrap_or_helper_removed() { - let source = load_program("should_fail_result_unwrap_or_removed.cap"); - let module = parse_module(&source).expect("parse module"); - let stdlib = load_stdlib().expect("load stdlib"); - let err = type_check_program(&module, &stdlib, &[]).expect_err("expected type error"); - assert!(err - .to_string() - .contains("unknown method `sys.result.Result__unwrap_or`")); -} - #[test] fn parse_if_let_fails() { let source = load_program("should_fail_if_let.cap"); diff --git a/docs/ABI.md b/docs/ABI.md index 5428c7a..3213bfd 100644 --- a/docs/ABI.md +++ b/docs/ABI.md @@ -33,11 +33,12 @@ compiler-generated stubs when needed. ## Allocation convention -The user-facing stdlib now defaults to the process allocator for ordinary code. -Explicit `Alloc` handles still appear in low-level APIs and `_with_alloc` -variants, and those handles are passed through to the runtime. The runtime -currently backs `Alloc` with libc `malloc`/`free`, but the ABI keeps explicit -allocator passing available for future custom or bounded allocators. +The user-facing stdlib defaults to the process allocator for ordinary code. +Explicit `Alloc` handles are reserved for controlled allocation paths. Prefer +`Alloc` receiver helpers where they exist; `_with_alloc` variants remain for +APIs that need to pass an allocator through to runtime-backed intrinsics. The +runtime currently backs `Alloc` with libc `malloc`/`free`, but the ABI keeps +explicit allocator passing available for future custom or bounded allocators. ## Status diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index d2b2e17..b164f4d 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -74,7 +74,7 @@ lexer ──> parser ──> AST ## Useful References - `TUTORIAL.md` for a quick language tour. -- `CURRENT_STATUS.md` for the implemented language/runtime behavior. -- `PROBLEMS.md` for the current design wrinkles. - `docs/POLICY.md` for safety and invariants. +- `docs/ABI.md` and `docs/memory.md` for runtime and ownership conventions. +- `stdlib/README.md` for stdlib API conventions and runtime-backed intrinsics. - `capc/tests/run.rs` for golden output expectations. diff --git a/docs/TUTORIAL.md b/docs/TUTORIAL.md index 8f0558e..52bf88a 100644 --- a/docs/TUTORIAL.md +++ b/docs/TUTORIAL.md @@ -344,9 +344,10 @@ fn build_greeting() -> Result { ``` Helpers: -- `string.split`, `split_once`, `trim_*`, `contains`, `index_of_*`. +- `string.split`, `split_once`, and `trim_*` return views where possible. +- `copy_split_*`, `copy_trim_*`, and `copy_*` helpers allocate owned copies. - `string.concat(other)` creates a new owned string view. -- `string.copy_text()` makes an owned `Text` builder when you need one. +- `string.to_text()` makes an owned `Text` builder when you need one. - `Text.as_string()` borrows cheaply; `Text.copy_string()` allocates a copy. - `Vec.as_string()` borrows bytes as text; `Vec.copy_string()` allocates a copy. - `Text.slice_range` returns a `string` view into its buffer. diff --git a/docs/memory.md b/docs/memory.md index 9b258b3..c5481ad 100644 --- a/docs/memory.md +++ b/docs/memory.md @@ -23,7 +23,9 @@ reject unsafe dependencies (`--safe-only`, `audit`). - Ordinary code uses the process default allocator through stdlib helpers like `string::text_new()`, `vec::new()`, and `fs.read_to_string(...)`. - Explicit `Alloc` handles still exist for low-level control, testing, and - future bounded/custom allocators. + future bounded/custom allocators. Prefer `Alloc` receiver helpers where they + exist; `_with_alloc` APIs are the explicit escape hatch for APIs that must + pass an allocator through to runtime-backed intrinsics. - `sys::buffer` is the low-level memory layer. Most application code should not need to talk to it directly. @@ -40,6 +42,9 @@ Capable separates owned buffers from borrowed views. ### Borrowed - `string`, `Slice`, and `MutSlice` are non-owning views. - Safe indexing and slicing are bounds-checked. +- String helpers whose names include `_view`, plus default helpers like + `trim`, `split`, and `split_once`, avoid copying string pieces where possible. + Use `copy_*`, `to_text`, or `copy_string` when you need owned data. Because Capable does not have a full lifetime system, safe code is restricted from letting slices escape: diff --git a/examples/how_to_string/how_to_string.cap b/examples/how_to_string/how_to_string.cap index 5d6a3c8..e2155aa 100644 --- a/examples/how_to_string/how_to_string.cap +++ b/examples/how_to_string/how_to_string.cap @@ -6,20 +6,20 @@ use sys::string fn demo_string_view(c: Console) -> unit { let s = " hello,world \n" - let trimmed = s.trim_view() + let trimmed = s.trim() c.println("-- string view --") c.println(trimmed) c.println("len:") c.println_i32(trimmed.len()) - try let parts = trimmed.split_once_view(',') else { + try let parts = trimmed.split_once(',') else { c.println("comma not found") return } c.println(parts.left) c.println(parts.right) - let words = trimmed.split_view(',') + let words = trimmed.split(',') defer words.free() c.println("split count:") c.println_i32(words.len()) diff --git a/stdlib/README.md b/stdlib/README.md index bcf5cd4..0c9fc40 100644 --- a/stdlib/README.md +++ b/stdlib/README.md @@ -8,3 +8,14 @@ The single source of truth for these runtime-backed intrinsics is: - `capc/src/codegen/intrinsics.rs` Anything not listed there is a real Capable implementation. + +## API conventions + +- Default stdlib APIs use the process default allocator and are meant for + ordinary application code. +- Explicit `Alloc` paths are for controlled allocation. Prefer methods on + `Alloc` where the API has one; `_with_alloc` variants are the low-level + escape hatch for APIs that must pass an allocator to runtime intrinsics. +- String helpers whose names end in `_view` never copy string bytes. Default + string helpers also prefer views when a view can represent the result. +- Helpers named `copy_*`, `to_text`, or `copy_string` allocate owned data. diff --git a/stdlib/sys/buffer.cap b/stdlib/sys/buffer.cap index 1c3093a..08b65f5 100644 --- a/stdlib/sys/buffer.cap +++ b/stdlib/sys/buffer.cap @@ -85,6 +85,16 @@ impl Alloc { return MutSlice { ptr: ptr, len: len } } + /// Copy a byte slice into memory owned by this Alloc. + pub fn copy_slice(self, data: Slice) -> Result, AllocErr> { + return copy_slice(self, data) + } + + /// Copy bytes into a new owned string view using this Alloc. + pub fn copy_string_from_bytes(self, data: Slice) -> Result { + return string::copy_from_bytes_with_alloc(self, data) + } + /// Create a Vec with this Alloc (heap-backed). pub fn vec_u8_new(self) -> vec::Vec { return vec::new_with_alloc(self) diff --git a/stdlib/sys/path.cap b/stdlib/sys/path.cap index 58007a5..e9de049 100644 --- a/stdlib/sys/path.cap +++ b/stdlib/sys/path.cap @@ -175,7 +175,7 @@ pub fn replace_extension(raw_path: string, ext: string) -> string { /// `ext` may be provided with or without a leading `.`. pub fn replace_extension_with_alloc(alloc: buffer::Alloc, raw_path: string, ext: string) -> string { let cut = extension_cut(raw_path) - let out = string::text_new_with_alloc(alloc) + let out = alloc.text_new() defer out.free() if (cut > 0) { let prefix = match (raw_path.slice_range(0, cut)) { diff --git a/stdlib/sys/result.cap b/stdlib/sys/result.cap index e508ca5..844f621 100644 --- a/stdlib/sys/result.cap +++ b/stdlib/sys/result.cap @@ -2,6 +2,8 @@ package safe module sys::result +use sys::option + /// Result for fallible operations. pub enum Result { /// Success value. @@ -9,3 +11,53 @@ pub enum Result { /// Error value. Err(E) } + +impl Result { + /// True if Ok. + pub fn is_ok(self) -> bool { + return match self { + Ok(_) => { true } + Err(_) => { false } + } + } + + /// True if Err. + pub fn is_err(self) -> bool { + return match self { + Ok(_) => { false } + Err(_) => { true } + } + } + + /// Unwrap the Ok value or panic. + pub fn unwrap(self) -> T { + return match self { + Ok(val) => { val } + Err(_) => { panic() } + } + } + + /// Return the Ok value or a default. + pub fn unwrap_or(self, default: T) -> T { + return match self { + Ok(val) => { val } + Err(_) => { default } + } + } + + /// Convert to Option, dropping the error. + pub fn ok(self) -> option::Option { + return match self { + Ok(val) => { option::Option::Some(val) } + Err(_) => { option::Option::None } + } + } + + /// Convert to Option, dropping the success value. + pub fn err(self) -> option::Option { + return match self { + Ok(_) => { option::Option::None } + Err(err) => { option::Option::Some(err) } + } + } +} diff --git a/stdlib/sys/string.cap b/stdlib/sys/string.cap index 6a0f9e0..fee81ec 100644 --- a/stdlib/sys/string.cap +++ b/stdlib/sys/string.cap @@ -27,14 +27,19 @@ pub struct SplitOnce { /// Wrap a slice as a string view (no copy). /// The view is only valid while the slice's backing storage is alive. -pub fn from_bytes(bytes: Slice) -> Result { +pub fn view_from_bytes(bytes: Slice) -> Result { return Ok(string { bytes: bytes }) } +/// Copy a slice into a new owned string view using the process default allocator. +pub fn copy_from_bytes(bytes: Slice) -> Result { + return copy_from_bytes_with_alloc(buffer::default_alloc(), bytes) +} + /// Copy a slice into a new owned string view using the provided allocator. -pub fn from_bytes_copy(alloc: buffer::Alloc, bytes: Slice) -> Result { +pub fn copy_from_bytes_with_alloc(alloc: buffer::Alloc, bytes: Slice) -> Result { let owned = buffer::copy_slice(alloc, bytes)? - return Ok(string { bytes: owned }) + return view_from_bytes(owned) } /// Allocate a new empty Text using the process default allocator. @@ -65,16 +70,16 @@ pub fn text_from(s: string) -> Result { /// Copy a string view into a new Text using the provided allocator. pub fn text_from_with_alloc(alloc: buffer::Alloc, s: string) -> Result { - let out = text_new_with_alloc(alloc) + let out = alloc.text_new() out.push_str(s)? return Ok(out) } -fn build_range(alloc: buffer::Alloc, s: string, start: i32, end: i32) -> string { +fn copy_range(alloc: buffer::Alloc, s: string, start: i32, end: i32) -> string { if (end <= start) { return "" } - let buf = text_new_with_alloc(alloc) + let buf = alloc.text_new() let i = start while (i < end) { match (buf.push_byte(s.byte_at(i))) { @@ -227,7 +232,7 @@ impl Text { /// This allocates a new owned slice for the returned string. pub fn copy_string(self) -> Result { let owned = self.bytes.copy_slice()? - return from_bytes(owned) + return view_from_bytes(owned) } /// Free the underlying Vec using the allocator stored in its backing Vec. @@ -272,15 +277,25 @@ impl string { } /// Copy this string view into a new owned Text using the process default allocator. - pub fn copy_text(self) -> Result { + pub fn to_text(self) -> Result { return text_from(self) } /// Copy this string view into a new owned Text using the provided allocator. - pub fn copy_text_with_alloc(self, alloc: buffer::Alloc) -> Result { + pub fn to_text_with_alloc(self, alloc: buffer::Alloc) -> Result { return text_from_with_alloc(alloc, self) } + /// Alias for to_text. + pub fn copy_text(self) -> Result { + return self.to_text() + } + + /// Copy this string view into a new owned Text using the provided allocator. + pub fn copy_text_with_alloc(self, alloc: buffer::Alloc) -> Result { + return self.to_text_with_alloc(alloc) + } + /// Concatenate another string into a new owned string view using the process default allocator. pub fn concat(self, other: string) -> Result { return self.concat_with_alloc(buffer::default_alloc(), other) @@ -288,7 +303,7 @@ impl string { /// Concatenate another string into a new owned string view using the provided allocator. pub fn concat_with_alloc(self, alloc: buffer::Alloc, other: string) -> Result { - let out = text_new_with_alloc(alloc) + let out = alloc.text_new() out.push_str(self)? out.push_str(other)? return out.copy_string() @@ -309,13 +324,18 @@ impl string { return self.as_slice() } - /// Split on ASCII whitespace using the process default allocator. + /// Split on ASCII whitespace into views using the process default allocator. pub fn split_whitespace(self) -> Vec { - return self.split_whitespace_with_alloc(buffer::default_alloc()) + return self.split_whitespace_view() } - /// Split on ASCII whitespace using the provided allocator. - pub fn split_whitespace_with_alloc(self, alloc: buffer::Alloc) -> Vec { + /// Split on ASCII whitespace into owned copies using the process default allocator. + pub fn copy_split_whitespace(self) -> Vec { + return self.copy_split_whitespace_with_alloc(buffer::default_alloc()) + } + + /// Split on ASCII whitespace into owned copies using the provided allocator. + pub fn copy_split_whitespace_with_alloc(self, alloc: buffer::Alloc) -> Vec { let out = alloc.vec_string_new() let bytes = self.as_slice() let len = bytes.len() @@ -331,7 +351,7 @@ impl string { while (i < len && !bytes.at(i).is_whitespace()) { i = i + 1 } - let part = build_range(alloc, self, start, i) + let part = copy_range(alloc, self, start, i) match (out.push(part)) { Ok(_) => { } Err(_) => { panic() } @@ -371,11 +391,18 @@ impl string { return out } + /// Split into line views using the process default allocator. pub fn lines(self) -> Vec { - return self.lines_with_alloc(buffer::default_alloc()) + return self.lines_view() } - pub fn lines_with_alloc(self, alloc: buffer::Alloc) -> Vec { + /// Split into owned line copies using the process default allocator. + pub fn copy_lines(self) -> Vec { + return self.copy_lines_with_alloc(buffer::default_alloc()) + } + + /// Split into owned line copies using the provided allocator. + pub fn copy_lines_with_alloc(self, alloc: buffer::Alloc) -> Vec { let out = alloc.vec_string_new() let bytes = self.as_slice() let len = bytes.len() @@ -387,7 +414,7 @@ impl string { if (end > start && bytes.at(end - 1) == '\r') { end = end - 1 } - let part = build_range(alloc, self, start, end) + let part = copy_range(alloc, self, start, end) match (out.push(part)) { Ok(_) => { } Err(_) => { panic() } @@ -401,7 +428,7 @@ impl string { if (end > start && bytes.at(end - 1) == '\r') { end = end - 1 } - let part = build_range(alloc, self, start, end) + let part = copy_range(alloc, self, start, end) match (out.push(part)) { Ok(_) => { } Err(_) => { panic() } @@ -451,11 +478,18 @@ impl string { return out } + /// Split into views using the process default allocator. pub fn split(self, delim: u8) -> Vec { - return self.split_with_alloc(buffer::default_alloc(), delim) + return self.split_view(delim) + } + + /// Split into owned copies using the process default allocator. + pub fn copy_split(self, delim: u8) -> Vec { + return self.copy_split_with_alloc(buffer::default_alloc(), delim) } - pub fn split_with_alloc(self, alloc: buffer::Alloc, delim: u8) -> Vec { + /// Split into owned copies using the provided allocator. + pub fn copy_split_with_alloc(self, alloc: buffer::Alloc, delim: u8) -> Vec { let out = alloc.vec_string_new() let bytes = self.as_slice() let len = bytes.len() @@ -463,7 +497,7 @@ impl string { let i = 0 while (i < len) { if (bytes.at(i) == delim) { - let part = build_range(alloc, self, start, i) + let part = copy_range(alloc, self, start, i) match (out.push(part)) { Ok(_) => { } Err(_) => { panic() } @@ -472,7 +506,7 @@ impl string { } i = i + 1 } - let part = build_range(alloc, self, start, len) + let part = copy_range(alloc, self, start, len) match (out.push(part)) { Ok(_) => { } Err(_) => { panic() } @@ -511,20 +545,25 @@ impl string { return out } - /// Split once on the first matching delimiter using the process default allocator. + /// Split once on the first matching delimiter without copying. pub fn split_once(self, delim: u8) -> Result { - return self.split_once_with_alloc(buffer::default_alloc(), delim) + return self.split_once_view(delim) } - /// Split once on the first matching delimiter using the provided allocator. - pub fn split_once_with_alloc(self, alloc: buffer::Alloc, delim: u8) -> Result { + /// Split once on the first matching delimiter into owned copies using the process default allocator. + pub fn copy_split_once(self, delim: u8) -> Result { + return self.copy_split_once_with_alloc(buffer::default_alloc(), delim) + } + + /// Split once on the first matching delimiter into owned copies using the provided allocator. + pub fn copy_split_once_with_alloc(self, alloc: buffer::Alloc, delim: u8) -> Result { let bytes = self.as_slice() let len = bytes.len() let i = 0 while (i < len) { if (bytes.at(i) == delim) { - let left = build_range(alloc, self, 0, i) - let right = build_range(alloc, self, i + 1, len) + let left = copy_range(alloc, self, 0, i) + let right = copy_range(alloc, self, i + 1, len) return Ok(SplitOnce { left: left, right: right @@ -595,70 +634,58 @@ impl string { return view_range(self, 0, i) } - /// Trim ASCII whitespace from both ends using the process default allocator. + /// Trim ASCII whitespace from both ends without copying. pub fn trim(self) -> string { - return self.trim_with_alloc(buffer::default_alloc()) + return self.trim_view() + } + + /// Trim ASCII whitespace from both ends into an owned copy using the process default allocator. + pub fn copy_trim(self) -> string { + return self.copy_trim_with_alloc(buffer::default_alloc()) } - /// Trim ASCII whitespace from both ends using the provided allocator. - pub fn trim_with_alloc(self, alloc: buffer::Alloc) -> string { - let start_trimmed = self.trim_start_with_alloc(alloc) - return start_trimmed.trim_end_with_alloc(alloc) + /// Trim ASCII whitespace from both ends into an owned copy using the provided allocator. + pub fn copy_trim_with_alloc(self, alloc: buffer::Alloc) -> string { + let start_trimmed = self.trim_start_view() + let trimmed = start_trimmed.trim_end_view() + return copy_range(alloc, trimmed, 0, trimmed.len()) } - /// Trim ASCII whitespace from the start using the process default allocator. + /// Trim ASCII whitespace from the start without copying. pub fn trim_start(self) -> string { - return self.trim_start_with_alloc(buffer::default_alloc()) + return self.trim_start_view() } - /// Trim ASCII whitespace from the start using the provided allocator. - pub fn trim_start_with_alloc(self, alloc: buffer::Alloc) -> string { - let bytes = self.as_slice() - let len = bytes.len() - let i = 0 - while (i < len) { - if (!bytes.at(i).is_whitespace()) { - break - } - i = i + 1 - } - if (i == 0) { - return self - } - return build_range(alloc, self, i, len) + /// Trim ASCII whitespace from the start into an owned copy using the process default allocator. + pub fn copy_trim_start(self) -> string { + return self.copy_trim_start_with_alloc(buffer::default_alloc()) } - /// Trim ASCII whitespace from the end using the process default allocator. + /// Trim ASCII whitespace from the start into an owned copy using the provided allocator. + pub fn copy_trim_start_with_alloc(self, alloc: buffer::Alloc) -> string { + let trimmed = self.trim_start_view() + return copy_range(alloc, trimmed, 0, trimmed.len()) + } + + /// Trim ASCII whitespace from the end without copying. pub fn trim_end(self) -> string { - return self.trim_end_with_alloc(buffer::default_alloc()) + return self.trim_end_view() } - /// Trim ASCII whitespace from the end using the provided allocator. - pub fn trim_end_with_alloc(self, alloc: buffer::Alloc) -> string { - let bytes = self.as_slice() - let len = bytes.len() - if (len == 0) { - return self - } - let i = len - while (i > 0) { - if (!bytes.at(i - 1).is_whitespace()) { - break - } - i = i - 1 - } - if (i == len) { - return self - } - if (i == 0) { - return "" - } - return build_range(alloc, self, 0, i) + /// Trim ASCII whitespace from the end into an owned copy using the process default allocator. + pub fn copy_trim_end(self) -> string { + return self.copy_trim_end_with_alloc(buffer::default_alloc()) + } + + /// Trim ASCII whitespace from the end into an owned copy using the provided allocator. + pub fn copy_trim_end_with_alloc(self, alloc: buffer::Alloc) -> string { + let trimmed = self.trim_end_view() + return copy_range(alloc, trimmed, 0, trimmed.len()) } - /// Remove a leading prefix if present using the process default allocator. + /// Remove a leading prefix if present without copying. pub fn trim_prefix(self, prefix: string) -> string { - return self.trim_prefix_with_alloc(buffer::default_alloc(), prefix) + return self.trim_prefix_view(prefix) } /// Remove a leading prefix if present without copying. @@ -669,17 +696,20 @@ impl string { return self } - /// Remove a leading prefix if present using the provided allocator. - pub fn trim_prefix_with_alloc(self, alloc: buffer::Alloc, prefix: string) -> string { - if (self.starts_with(prefix)) { - return build_range(alloc, self, prefix.len(), self.len()) - } - return self + /// Remove a leading prefix if present into an owned copy using the process default allocator. + pub fn copy_trim_prefix(self, prefix: string) -> string { + return self.copy_trim_prefix_with_alloc(buffer::default_alloc(), prefix) } - /// Remove a trailing suffix if present using the process default allocator. + /// Remove a leading prefix if present into an owned copy using the provided allocator. + pub fn copy_trim_prefix_with_alloc(self, alloc: buffer::Alloc, prefix: string) -> string { + let trimmed = self.trim_prefix_view(prefix) + return copy_range(alloc, trimmed, 0, trimmed.len()) + } + + /// Remove a trailing suffix if present without copying. pub fn trim_suffix(self, suffix: string) -> string { - return self.trim_suffix_with_alloc(buffer::default_alloc(), suffix) + return self.trim_suffix_view(suffix) } /// Remove a trailing suffix if present without copying. @@ -690,12 +720,15 @@ impl string { return self } - /// Remove a trailing suffix if present using the provided allocator. - pub fn trim_suffix_with_alloc(self, alloc: buffer::Alloc, suffix: string) -> string { - if (self.ends_with(suffix)) { - return build_range(alloc, self, 0, self.len() - suffix.len()) - } - return self + /// Remove a trailing suffix if present into an owned copy using the process default allocator. + pub fn copy_trim_suffix(self, suffix: string) -> string { + return self.copy_trim_suffix_with_alloc(buffer::default_alloc(), suffix) + } + + /// Remove a trailing suffix if present into an owned copy using the provided allocator. + pub fn copy_trim_suffix_with_alloc(self, alloc: buffer::Alloc, suffix: string) -> string { + let trimmed = self.trim_suffix_view(suffix) + return copy_range(alloc, trimmed, 0, trimmed.len()) } /// split_lines() is an alias for lines(). @@ -703,9 +736,14 @@ impl string { return self.lines() } - /// split_lines() using the provided allocator. - pub fn split_lines_with_alloc(self, alloc: buffer::Alloc) -> Vec { - return self.lines_with_alloc(alloc) + /// Copy split_lines() using the process default allocator. + pub fn copy_split_lines(self) -> Vec { + return self.copy_lines() + } + + /// Copy split_lines() using the provided allocator. + pub fn copy_split_lines_with_alloc(self, alloc: buffer::Alloc) -> Vec { + return self.copy_lines_with_alloc(alloc) } /// True if the string starts with the prefix. @@ -998,7 +1036,7 @@ impl string { pub fn to_lower_ascii_with_alloc(self, alloc: buffer::Alloc) -> string { let bytes = self.as_slice() let len = bytes.len() - let buf = text_new_with_alloc(alloc) + let buf = alloc.text_new() let i = 0 while (i < len) { let b = bytes.at(i) @@ -1024,7 +1062,7 @@ impl string { pub fn to_upper_ascii_with_alloc(self, alloc: buffer::Alloc) -> string { let bytes = self.as_slice() let len = bytes.len() - let buf = text_new_with_alloc(alloc) + let buf = alloc.text_new() let i = 0 while (i < len) { let b = bytes.at(i) @@ -1041,14 +1079,19 @@ impl string { } } - /// Trim ASCII whitespace (alias of trim()). + /// Trim ASCII whitespace without copying (alias of trim()). pub fn trim_ascii(self) -> string { return self.trim() } - /// Trim ASCII whitespace using the provided allocator. - pub fn trim_ascii_with_alloc(self, alloc: buffer::Alloc) -> string { - return self.trim_with_alloc(alloc) + /// Trim ASCII whitespace into an owned copy using the process default allocator. + pub fn copy_trim_ascii(self) -> string { + return self.copy_trim() + } + + /// Trim ASCII whitespace into an owned copy using the provided allocator. + pub fn copy_trim_ascii_with_alloc(self, alloc: buffer::Alloc) -> string { + return self.copy_trim_with_alloc(alloc) } /// Alias for index_of_byte. diff --git a/stdlib/sys/vec.cap b/stdlib/sys/vec.cap index f49b40d..a8de693 100644 --- a/stdlib/sys/vec.cap +++ b/stdlib/sys/vec.cap @@ -635,7 +635,7 @@ impl Vec { /// Borrow the bytes as a string view (no copy). /// The view is invalid after freeing this Vec. pub fn as_string(self) -> string { - return match (string::from_bytes(self.as_slice())) { + return match (string::view_from_bytes(self.as_slice())) { Ok(s) => { s } Err(_) => { panic() } } @@ -659,7 +659,7 @@ impl Vec { /// Copy contents into a new owned string view. pub fn copy_string(self) -> Result { let owned = self.copy_slice()? - return string::from_bytes(owned) + return string::view_from_bytes(owned) } } diff --git a/tests/programs/result_helpers.cap b/tests/programs/result_helpers.cap new file mode 100644 index 0000000..95f0d76 --- /dev/null +++ b/tests/programs/result_helpers.cap @@ -0,0 +1,46 @@ +package safe +module result_helpers + +use sys::option +use sys::system + +fn make(flag: bool) -> Result { + if (flag) { + return Ok(7) + } + return Err("bad") +} + +pub fn main(rc: RootCap) -> i32 { + let c = rc.mint_console() + + c.assert(make(true).is_ok()) + c.assert(!make(true).is_err()) + c.assert(make(false).is_err()) + c.assert(!make(false).is_ok()) + c.assert(make(true).unwrap() == 7) + c.assert(make(false).unwrap_or(9) == 9) + + match (make(true).ok()) { + option::Option::Some(v) => { c.assert(v == 7) } + option::Option::None => { c.assert(false) } + } + + match (make(false).ok()) { + option::Option::Some(_) => { c.assert(false) } + option::Option::None => { c.assert(true) } + } + + match (make(false).err()) { + option::Option::Some(e) => { c.assert(e.eq("bad")) } + option::Option::None => { c.assert(false) } + } + + match (make(true).err()) { + option::Option::Some(_) => { c.assert(false) } + option::Option::None => { c.assert(true) } + } + + c.println("result helpers ok") + return 0 +} diff --git a/tests/programs/should_fail_result_ok_removed.cap b/tests/programs/should_fail_result_ok_removed.cap deleted file mode 100644 index 68674f5..0000000 --- a/tests/programs/should_fail_result_ok_removed.cap +++ /dev/null @@ -1,10 +0,0 @@ -module should_fail_result_ok_removed - -fn make() -> Result { - return Ok(1) -} - -pub fn main() -> i32 { - let v = make().ok() - return v -} diff --git a/tests/programs/should_fail_result_unwrap_or_removed.cap b/tests/programs/should_fail_result_unwrap_or_removed.cap deleted file mode 100644 index a220017..0000000 --- a/tests/programs/should_fail_result_unwrap_or_removed.cap +++ /dev/null @@ -1,10 +0,0 @@ -module should_fail_result_unwrap_or_removed - -fn make() -> Result { - return Ok(1) -} - -pub fn main() -> i32 { - let v = make().unwrap_or(9) - return v -} diff --git a/tests/programs/string_helpers.cap b/tests/programs/string_helpers.cap index 269b17a..3837e3e 100644 --- a/tests/programs/string_helpers.cap +++ b/tests/programs/string_helpers.cap @@ -10,27 +10,37 @@ pub fn main(rc: RootCap) -> i32 { let n = buf.len() let b = buf.at(0) let words = "a b c".split_whitespace() + let word_copies = "a b c".copy_split_whitespace() let word_views = "a b c".split_whitespace_view() let split_views = "a,b,c".split_view(',') defer words.free() + defer word_copies.free() defer word_views.free() defer split_views.free() let count = words.len() let trimmed = " hi \n".trim() let trimmed_view = " hi \n".trim_view() + let trimmed_copy = " hi \n".copy_trim() let trimmed_start = " hi ".trim_start() let trimmed_start_view = " hi ".trim_start_view() + let trimmed_start_copy = " hi ".copy_trim_start() let trimmed_end = " hi ".trim_end() let trimmed_end_view = " hi ".trim_end_view() + let trimmed_end_copy = " hi ".copy_trim_end() let trimmed_prefix_view = "title: Hello".trim_prefix_view("title:") + let trimmed_prefix_copy = "title: Hello".copy_trim_prefix("title:") let trimmed_suffix_view = "hello.html".trim_suffix_view(".html") + let trimmed_suffix_copy = "hello.html".copy_trim_suffix(".html") let trimmed_ascii = " \tHi\n".trim_ascii() + let trimmed_ascii_copy = " \tHi\n".copy_trim_ascii() let lower = "AbC".to_lower_ascii() let upper = "AbC".to_upper_ascii() let sliced = "hello".slice_range(1, 4) let lines = "a\nb\n".split_lines() + let line_copies = "a\r\nb\n".copy_lines() let line_views = "a\r\nb\n".lines_view() defer lines.free() + defer line_copies.free() defer line_views.free() let t = string::text_new() defer t.free() @@ -43,19 +53,26 @@ pub fn main(rc: RootCap) -> i32 { } c.assert(owned.eq("hi")) c.assert(n == 3 && b == 'a' && count == 3) + c.assert(word_copies.len() == 3) c.assert(word_views.len() == 3) c.assert(split_views.len() == 3) c.assert(trimmed.len() == 2) c.assert(trimmed_view.eq("hi")) + c.assert(trimmed_copy.eq("hi")) c.assert(trimmed.starts_with_byte('h')) c.assert(trimmed.ends_with_byte('i')) c.assert(trimmed_start.starts_with("hi")) c.assert(trimmed_start_view.starts_with("hi")) + c.assert(trimmed_start_copy.starts_with("hi")) c.assert(trimmed_end.ends_with("hi")) c.assert(trimmed_end_view.ends_with("hi")) + c.assert(trimmed_end_copy.ends_with("hi")) c.assert(trimmed_prefix_view.trim_view().eq("Hello")) + c.assert(trimmed_prefix_copy.trim().eq("Hello")) c.assert(trimmed_suffix_view.eq("hello")) + c.assert(trimmed_suffix_copy.eq("hello")) c.assert(trimmed_ascii.eq("Hi")) + c.assert(trimmed_ascii_copy.eq("Hi")) c.assert(lower.eq("abc")) c.assert(upper.eq("ABC")) c.assert("abc".starts_with_byte('a')) @@ -129,9 +146,13 @@ pub fn main(rc: RootCap) -> i32 { Err(_) => { c.assert(false) } } let pieces = "a,b,c".split(',') + let piece_copies = "a,b,c".copy_split(',') defer pieces.free() + defer piece_copies.free() c.assert(pieces.len() == 3) + c.assert(piece_copies.len() == 3) c.assert(line_views.len() == 2) + c.assert(line_copies.len() == 2) match (pieces.join(",")) { Ok(joined) => { c.assert(joined.eq("a,b,c")) } Err(_) => { c.assert(false) } diff --git a/tests/programs/text_helpers_more.cap b/tests/programs/text_helpers_more.cap index f2b5a12..170e948 100644 --- a/tests/programs/text_helpers_more.cap +++ b/tests/programs/text_helpers_more.cap @@ -44,7 +44,7 @@ pub fn main(rc: RootCap) -> i32 { return 1 } c.assert(owned2.eq("cap")) - try let t3 = "owned".copy_text() else { + try let t3 = "owned".to_text() else { panic() } defer t3.free()