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
|
/// The exact evaluation core. Produces a `Number`, staying exact until an
|
||||||
/// operation forces the float fallback (see design.md 2.7.4).
|
/// 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
|
/// 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
|
/// 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
|
/// the engine, and by the time both frontends had moved to `evalString` nothing
|
||||||
/// called it.
|
/// 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.*) {
|
switch (expr.*) {
|
||||||
.number => |n| return literalToNumber(scratch, n),
|
.number => |n| return literalToNumber(scratch, n),
|
||||||
.string_literal => |text| {
|
.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;
|
const value = env.getVar(name) orelse return Error.UnknownVariable;
|
||||||
return try value.cloneWith(scratch);
|
return try value.cloneWith(scratch);
|
||||||
},
|
},
|
||||||
.assignment => |a| {
|
.assignment => {
|
||||||
// A built-in name is answered by `getVar` before the variable map, so
|
// The parser produces an assignment only as a whole statement (Task 5.19),
|
||||||
// storing one would be write-only: `pi = 3` used to return 3 and leave
|
// and `evaluateStatement` unwraps that one before this walk starts. Reaching
|
||||||
// pi untouched, with nothing to tell the user the name had not taken.
|
// one here means a tree the parser cannot build; an error rather than
|
||||||
if (Environment.isBuiltIn(a.name)) return Error.AssignmentToConstant;
|
// `unreachable`, because a hand-built tree is a caller's bug, not undefined
|
||||||
const val = try evalExact(env, scratch, a.value);
|
// behaviour in a release build.
|
||||||
// setVar copies, so storing an arena-allocated value is safe. What comes
|
return Error.UnexpectedToken;
|
||||||
// back is the arena's copy, not the environment's.
|
|
||||||
env.setVar(a.name, val) catch return Error.OutOfMemory;
|
|
||||||
return val;
|
|
||||||
},
|
},
|
||||||
.unary => |u| {
|
.unary => |u| {
|
||||||
const operand = try evalExact(env, scratch, u.operand);
|
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
|
/// The name and the argument count are checked before anything is evaluated, so a
|
||||||
/// misspelled name and a miscounted argument list are different errors.
|
/// 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 builtin = std.meta.stringToEnum(Builtin, name) orelse return Error.UnknownFunction;
|
||||||
const arity = arityOf(builtin);
|
const arity = arityOf(builtin);
|
||||||
if (args.len < arity.min or args.len > arity.max) return Error.WrongArgumentCount;
|
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
|
/// 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.
|
/// result, so `sqrt(4)` is 2 and `factorial(171)` is every one of its 310 digits.
|
||||||
fn exactBuiltin(
|
fn exactBuiltin(
|
||||||
env: *Environment,
|
env: *const Environment,
|
||||||
scratch: Allocator,
|
scratch: Allocator,
|
||||||
builtin: Builtin,
|
builtin: Builtin,
|
||||||
args: []const *Expr,
|
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
|
/// Like evalString but returns metadata (whether the expression used
|
||||||
/// non-decimal literals) so frontends can decide to show a multi-base view.
|
/// 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 {
|
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);
|
var p = Parser.init(allocator, source);
|
||||||
const expr = try p.parse();
|
const expr = try p.parse();
|
||||||
// The parser hands over ownership. Nothing in the result borrows from the
|
// 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();
|
defer arena.deinit();
|
||||||
const scratch = arena.allocator();
|
const scratch = arena.allocator();
|
||||||
|
|
||||||
const raw = try evalExact(env, scratch, expr);
|
const statement = try evaluateStatement(env, scratch, expr);
|
||||||
const result = try raw.cloneWith(allocator);
|
var result = try statement.value.cloneWith(allocator);
|
||||||
|
errdefer result.deinit();
|
||||||
|
|
||||||
|
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;
|
env.setAns(result) catch return Error.OutOfMemory;
|
||||||
|
}
|
||||||
return .{
|
return .{
|
||||||
.value = result,
|
.value = result,
|
||||||
.has_nondecimal_literal = hasNonDecimalLiteral(expr),
|
.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.
|
/// Walk an AST and report whether any number literal is non-decimal.
|
||||||
fn hasNonDecimalLiteral(expr: *const Expr) bool {
|
fn hasNonDecimalLiteral(expr: *const Expr) bool {
|
||||||
return switch (expr.*) {
|
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));
|
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" {
|
test "isBuiltIn covers exactly the names getVar answers itself" {
|
||||||
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
|
||||||
defer _ = arena.deinit();
|
defer _ = arena.deinit();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue