allow for preview evaluations/partial results to show in other UI surfaces
This commit is contained in:
parent
d36cf386ac
commit
f44d51c219
1 changed files with 153 additions and 16 deletions
|
|
@ -155,11 +155,16 @@ pub const Environment = struct {
|
|||
/// The exact evaluation core. Produces a `Number`, staying exact until an
|
||||
/// operation forces the float fallback (see design.md 2.7.4).
|
||||
///
|
||||
/// The environment is `const`: this walk reads variables and `Ans` and never stores
|
||||
/// anything. Storing is the statement's job, done once at the root by
|
||||
/// `evalStringInfo`, which is what lets `previewStringInfo` share this walk and be
|
||||
/// unable to change the session rather than merely careful not to (design 6.5).
|
||||
///
|
||||
/// There used to be a `pub fn evaluate` above this that ran the same walk and
|
||||
/// collapsed the result to `f64`. It was the last f64-returning expression API in
|
||||
/// the engine, and by the time both frontends had moved to `evalString` nothing
|
||||
/// called it.
|
||||
fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) Error!Number {
|
||||
fn evalExact(env: *const Environment, scratch: Allocator, expr: *const Expr) Error!Number {
|
||||
switch (expr.*) {
|
||||
.number => |n| return literalToNumber(scratch, n),
|
||||
.string_literal => |text| {
|
||||
|
|
@ -178,16 +183,13 @@ fn evalExact(env: *Environment, scratch: Allocator, expr: *const Expr) Error!Num
|
|||
const value = env.getVar(name) orelse return Error.UnknownVariable;
|
||||
return try value.cloneWith(scratch);
|
||||
},
|
||||
.assignment => |a| {
|
||||
// A built-in name is answered by `getVar` before the variable map, so
|
||||
// storing one would be write-only: `pi = 3` used to return 3 and leave
|
||||
// pi untouched, with nothing to tell the user the name had not taken.
|
||||
if (Environment.isBuiltIn(a.name)) return Error.AssignmentToConstant;
|
||||
const val = try evalExact(env, scratch, a.value);
|
||||
// setVar copies, so storing an arena-allocated value is safe. What comes
|
||||
// back is the arena's copy, not the environment's.
|
||||
env.setVar(a.name, val) catch return Error.OutOfMemory;
|
||||
return val;
|
||||
.assignment => {
|
||||
// The parser produces an assignment only as a whole statement (Task 5.19),
|
||||
// and `evaluateStatement` unwraps that one before this walk starts. Reaching
|
||||
// one here means a tree the parser cannot build; an error rather than
|
||||
// `unreachable`, because a hand-built tree is a caller's bug, not undefined
|
||||
// behaviour in a release build.
|
||||
return Error.UnexpectedToken;
|
||||
},
|
||||
.unary => |u| {
|
||||
const operand = try evalExact(env, scratch, u.operand);
|
||||
|
|
@ -467,7 +469,7 @@ fn arityOf(builtin: Builtin) Arity {
|
|||
///
|
||||
/// The name and the argument count are checked before anything is evaluated, so a
|
||||
/// misspelled name and a miscounted argument list are different errors.
|
||||
fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: []const *Expr) Error!Number {
|
||||
fn evalFunction(env: *const Environment, scratch: Allocator, name: []const u8, args: []const *Expr) Error!Number {
|
||||
const builtin = std.meta.stringToEnum(Builtin, name) orelse return Error.UnknownFunction;
|
||||
const arity = arityOf(builtin);
|
||||
if (args.len < arity.min or args.len > arity.max) return Error.WrongArgumentCount;
|
||||
|
|
@ -492,7 +494,7 @@ fn evalFunction(env: *Environment, scratch: Allocator, name: []const u8, args: [
|
|||
/// The built-ins with an exact implementation: an exact operand gives an exact
|
||||
/// result, so `sqrt(4)` is 2 and `factorial(171)` is every one of its 310 digits.
|
||||
fn exactBuiltin(
|
||||
env: *Environment,
|
||||
env: *const Environment,
|
||||
scratch: Allocator,
|
||||
builtin: Builtin,
|
||||
args: []const *Expr,
|
||||
|
|
@ -653,7 +655,35 @@ pub fn evalString(env: *Environment, allocator: Allocator, source: []const u8) E
|
|||
|
||||
/// Like evalString but returns metadata (whether the expression used
|
||||
/// non-decimal literals) so frontends can decide to show a multi-base view.
|
||||
///
|
||||
/// Commits: an assignment stores its variable, and the value becomes `Ans`.
|
||||
pub fn evalStringInfo(env: *Environment, allocator: Allocator, source: []const u8) Error!EvalInfo {
|
||||
return evalSource(.commit, env, allocator, source);
|
||||
}
|
||||
|
||||
/// What `evalStringInfo` would answer, with nothing stored (design 6.5).
|
||||
///
|
||||
/// For a screen that answers as the user types. An assignment previews as the value
|
||||
/// it would store and stores nothing; `Ans` keeps the last committed answer. The
|
||||
/// environment is `const`, so that is a property of the signature rather than of
|
||||
/// this function's care.
|
||||
pub fn previewStringInfo(env: *const Environment, allocator: Allocator, source: []const u8) Error!EvalInfo {
|
||||
return evalSource(.preview, env, allocator, source);
|
||||
}
|
||||
|
||||
/// Whether an evaluation is allowed to change the environment.
|
||||
const Effect = enum { commit, preview };
|
||||
|
||||
/// The environment pointer each effect is given. A preview gets a `*const`, so the
|
||||
/// commit branch below is not merely skipped for it but could not compile.
|
||||
fn EnvFor(comptime effect: Effect) type {
|
||||
return switch (effect) {
|
||||
.commit => *Environment,
|
||||
.preview => *const Environment,
|
||||
};
|
||||
}
|
||||
|
||||
fn evalSource(comptime effect: Effect, env: EnvFor(effect), allocator: Allocator, source: []const u8) Error!EvalInfo {
|
||||
var p = Parser.init(allocator, source);
|
||||
const expr = try p.parse();
|
||||
// The parser hands over ownership. Nothing in the result borrows from the
|
||||
|
|
@ -669,16 +699,45 @@ pub fn evalStringInfo(env: *Environment, allocator: Allocator, source: []const u
|
|||
defer arena.deinit();
|
||||
const scratch = arena.allocator();
|
||||
|
||||
const raw = try evalExact(env, scratch, expr);
|
||||
const result = try raw.cloneWith(allocator);
|
||||
const statement = try evaluateStatement(env, scratch, expr);
|
||||
var result = try statement.value.cloneWith(allocator);
|
||||
errdefer result.deinit();
|
||||
|
||||
env.setAns(result) catch return Error.OutOfMemory;
|
||||
if (effect == .commit) {
|
||||
// Both copy, so storing an arena-allocated value is safe. The variable is
|
||||
// stored only once the value is known to have been copied out, so a failed
|
||||
// copy no longer leaves an assignment half-made.
|
||||
if (statement.assigns) |name| env.setVar(name, statement.value) catch return Error.OutOfMemory;
|
||||
env.setAns(result) catch return Error.OutOfMemory;
|
||||
}
|
||||
return .{
|
||||
.value = result,
|
||||
.has_nondecimal_literal = hasNonDecimalLiteral(expr),
|
||||
};
|
||||
}
|
||||
|
||||
/// A whole input's value, and the variable it would store if it is an assignment.
|
||||
const Statement = struct {
|
||||
value: Number,
|
||||
/// Points into the source text; `setVar` copies it.
|
||||
assigns: ?[]const u8,
|
||||
};
|
||||
|
||||
/// Evaluate the root of a tree, which is the only place an assignment can be.
|
||||
fn evaluateStatement(env: *const Environment, scratch: Allocator, expr: *const Expr) Error!Statement {
|
||||
switch (expr.*) {
|
||||
.assignment => |a| {
|
||||
// A built-in name is answered by `getVar` before the variable map, so
|
||||
// storing one would be write-only: `pi = 3` used to return 3 and leave
|
||||
// pi untouched, with nothing to tell the user the name had not taken.
|
||||
// Checked here rather than at the store, so a preview reports it too.
|
||||
if (Environment.isBuiltIn(a.name)) return Error.AssignmentToConstant;
|
||||
return .{ .value = try evalExact(env, scratch, a.value), .assigns = a.name };
|
||||
},
|
||||
else => return .{ .value = try evalExact(env, scratch, expr), .assigns = null },
|
||||
}
|
||||
}
|
||||
|
||||
/// Walk an AST and report whether any number literal is non-decimal.
|
||||
fn hasNonDecimalLiteral(expr: *const Expr) bool {
|
||||
return switch (expr.*) {
|
||||
|
|
@ -1924,6 +1983,84 @@ test "a constant keeps its value, and a variable of another name still assigns"
|
|||
try testing.expectEqual(@as(f64, 3), assigned.toFloat(alloc));
|
||||
}
|
||||
|
||||
// -- Preview: the same answer, nothing stored (design 6.5) --
|
||||
|
||||
/// Preview `source` and return its value as a float, freeing everything it allocated.
|
||||
fn previewFloat(env: *const Environment, source: []const u8) !f64 {
|
||||
var info = try previewStringInfo(env, testing.allocator, source);
|
||||
defer info.value.deinit();
|
||||
return info.value.toFloat(testing.allocator);
|
||||
}
|
||||
|
||||
fn commitFloat(env: *Environment, source: []const u8) !f64 {
|
||||
var value = try evalString(env, testing.allocator, source);
|
||||
defer value.deinit();
|
||||
return value.toFloat(testing.allocator);
|
||||
}
|
||||
|
||||
test "a previewed assignment shows its value and stores nothing" {
|
||||
var env = Environment.init(testing.allocator);
|
||||
defer env.deinit();
|
||||
|
||||
// What a live result shows while `x = 7` is being typed.
|
||||
try testing.expectEqual(@as(f64, 7), try previewFloat(&env, "x = 7"));
|
||||
try testing.expectEqual(@as(u32, 0), env.variables.count());
|
||||
try testing.expectError(Error.UnknownVariable, previewFloat(&env, "x"));
|
||||
|
||||
// Committing it is what stores it, and a preview can then read it.
|
||||
try testing.expectEqual(@as(f64, 7), try commitFloat(&env, "x = 7"));
|
||||
try testing.expectEqual(@as(f64, 42), try previewFloat(&env, "x * 6"));
|
||||
|
||||
// A previewed reassignment leaves the committed value alone.
|
||||
try testing.expectEqual(@as(f64, 100), try previewFloat(&env, "x = 100"));
|
||||
try testing.expectEqual(@as(f64, 7), try commitFloat(&env, "x"));
|
||||
}
|
||||
|
||||
test "a preview leaves Ans as the last committed answer" {
|
||||
var env = Environment.init(testing.allocator);
|
||||
defer env.deinit();
|
||||
|
||||
try testing.expectEqual(@as(f64, 5), try commitFloat(&env, "2 + 3"));
|
||||
// Every keystroke of `Ans + 100` is previewed; none of them is the new `Ans`.
|
||||
try testing.expectEqual(@as(f64, 105), try previewFloat(&env, "Ans + 100"));
|
||||
try testing.expectEqual(@as(f64, 105), try previewFloat(&env, "Ans + 100"));
|
||||
try testing.expectEqual(@as(f64, 5), try commitFloat(&env, "Ans"));
|
||||
}
|
||||
|
||||
test "a preview reports the errors committing would" {
|
||||
var env = Environment.init(testing.allocator);
|
||||
defer env.deinit();
|
||||
|
||||
// The built-in check happens before the store, so a preview sees it too.
|
||||
try testing.expectError(Error.AssignmentToConstant, previewFloat(&env, "pi = 3"));
|
||||
// An unfinished expression is an error, which a live result shows as nothing.
|
||||
try testing.expectError(Error.UnexpectedEnd, previewFloat(&env, "2 +"));
|
||||
try testing.expectError(Error.DivisionByZero, previewFloat(&env, "1 / 0"));
|
||||
}
|
||||
|
||||
test "a preview keeps the multi-base metadata" {
|
||||
var env = Environment.init(testing.allocator);
|
||||
defer env.deinit();
|
||||
|
||||
var info = try previewStringInfo(&env, testing.allocator, "0xFF + 1");
|
||||
defer info.value.deinit();
|
||||
try testing.expect(info.has_nondecimal_literal);
|
||||
}
|
||||
|
||||
test "an assignment below the root is refused rather than evaluated" {
|
||||
// The parser cannot build this tree (assignment is a statement), so only a
|
||||
// hand-built one reaches the walk. It must not be able to store from there,
|
||||
// and it cannot: the walk takes a `*const Environment`.
|
||||
var env = Environment.init(testing.allocator);
|
||||
defer env.deinit();
|
||||
|
||||
var value: Expr = .{ .number = .{ .base = .decimal, .text = "1" } };
|
||||
const nested: Expr = .{ .assignment = .{ .name = "x", .value = &value } };
|
||||
var arena = std.heap.ArenaAllocator.init(testing.allocator);
|
||||
defer arena.deinit();
|
||||
try testing.expectError(Error.UnexpectedToken, evalExact(&env, arena.allocator(), &nested));
|
||||
}
|
||||
|
||||
test "isBuiltIn covers exactly the names getVar answers itself" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||
defer _ = arena.deinit();
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue